Audit
This library provides a NestJS module to handle audit logging in our backend services.
We should audit all authenticated endpoints representing user actions. We should also audit authenticated endpoints handling sensitive user data, and data that we own. In the future, we'll trust other government organisations to correctly audit delegated requests to data they own.
There are a few different ways to log an action depending on what you're doing, but in the end an audit entry consists of these fields:
user
- Information about the authenticated user, as provided by the IdsAuth guard and @CurrentUser param decorator.namespace
- A namespace for the action, to prevent conflicts between different audit logs from different APIs. Formatted like this:@domain.is/subNamespace
action
- The action performed by the user. Should be camelCase and start with a verb.resources
- Optional: One or more resource ids affected by the action.meta
- Optional: An object of extra information specific to the action.
Import the audit module in your root module like this:
@Module({
imports: [
AuditModule.forRoot({
groupName: 'CloudWatch Logs Group Name',
serviceName: 'api-name',
defaultNamespace: '@island.is/apiName',
}),
],
})
export class AppModule {}
The
groupName
and serviceName
affect which Cloudwatch Logs group and stream (respectively) the audit logs are stored in. When NODE_ENV !== 'production'
, you can skip these options in which case audit entries are logged with our Logger module to the console.The optional
defaultNamespace
option provides a default namespace for every audit entry logged. It can still be overridden as needed.Make sure to inject the AuditService and CurrentUser:
import { AuditService } from '@island.is/nest/audit'
@Controller()
class MyController {
constructor(private auditService: AuditService) {}
@Get('stuff')
async findAll(
@CurrentUser()
user: User,
) {
// ...
}
}
Then you can create audit records like this:
// uses the default namespace: '@island.is/apiName',
this.auditService.audit({
auth: user,
action: 'findAll',
})
You can set custom namespace, resources and metadata like this:
const stuff = await this.stuffService.getStuff()
this.auditService.audit({
auth: user,
namespace: '@island.is/overridden',
action: 'findAll',
resources: stuff.map((s) => s.id),
meta: { count: stuff.length },
})
If you are auditing an async action, you can wrap it like this:
return this.auditService.auditPromise(
{
auth: user,
action: 'findAll',
resources: (stuff) => stuff.map((s) => s.id),
meta: (stuff) => ({ count: stuff.length }),
},
this.stuffService.getStuff(),
)
For simple controllers/resolvers, you can enable audit with decorators like this:
import { Audit } from '@island.is/nest/audit'
@Controller()
class MyController {
@Get('stuff')
@Audit()
async findAll() {}
}
By default it will use the defaultNamespace, and the handler name as the action. You can override all of the audit entry fields at both the controller level and the handler level:
import { Audit } from '@island.is/nest/audit'
@Controller()
@Audit({ namespace: '@island.is/overridden' })
class MyController {
constructor(private stuffService: StuffService) {}
@Get('stuff')
@Audit<Stuff[]>({
action: 'findStuff',
resources: (stuff) => stuff.map((s) => s.id),
meta: (stuff) => ({ count: stuff.length }),
})
async findAll() {
return this.stuffService.getStuff()
}
}
If you want to include request arguments as
meta
, you should use the AuditService
methods instead.Last modified 1mo ago