Skip to content

Commit d886170

Browse files
authored
Store audit log in DynamoDB (#118)
* create audit log * reduce log retention days in prod * add simple unit test * replace audit log statements with dynamodb calls * update to use enum for module names * add audit log statements to linkry * remove unused imports in linkry module
1 parent 1741ba6 commit d886170

File tree

16 files changed

+323
-132
lines changed

16 files changed

+323
-132
lines changed

cloudformation/iam.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ Resources:
106106
Resource:
107107
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-rate-limiter
108108

109+
- Sid: DynamoDBAuditLogTableAccess
110+
Effect: Allow
111+
Action:
112+
- dynamodb:DescribeTable
113+
- dynamodb:PutItem
114+
- dynamodb:Query
115+
Resource:
116+
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-audit-log
117+
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-audit-log/index/*
118+
109119
- Sid: DynamoDBStreamAccess
110120
Effect: Allow
111121
Action:

cloudformation/logs.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,26 @@ Resources:
2121
LogGroupName:
2222
Fn::Sub: /aws/lambda/${LambdaFunctionName}-edge
2323
RetentionInDays: 7
24+
AppAuditLog:
25+
Type: "AWS::DynamoDB::Table"
26+
DeletionPolicy: "Retain"
27+
UpdateReplacePolicy: "Retain"
28+
Properties:
29+
BillingMode: "PAY_PER_REQUEST"
30+
TableName: infra-core-api-audit-log
31+
DeletionProtectionEnabled: true
32+
PointInTimeRecoverySpecification:
33+
PointInTimeRecoveryEnabled: true
34+
AttributeDefinitions:
35+
- AttributeName: module
36+
AttributeType: S
37+
- AttributeName: createdAt
38+
AttributeType: N
39+
KeySchema:
40+
- AttributeName: module
41+
KeyType: HASH
42+
- AttributeName: createdAt
43+
KeyType: RANGE
44+
TimeToLiveSpecification:
45+
AttributeName: expiresAt
46+
Enabled: true

cloudformation/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Mappings:
4848
LogRetentionDays: 7
4949
SesDomain: "aws.qa.acmuiuc.org"
5050
prod:
51-
LogRetentionDays: 365
51+
LogRetentionDays: 90
5252
SesDomain: "acm.illinois.edu"
5353
ApiGwConfig:
5454
dev:

src/api/functions/auditLog.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
2+
import { marshall } from "@aws-sdk/util-dynamodb";
3+
import { genericConfig } from "common/config.js";
4+
import { Modules } from "common/modules.js";
5+
6+
export type AuditLogEntry = {
7+
module: Modules;
8+
actor: string;
9+
target: string;
10+
requestId?: string;
11+
message: string;
12+
};
13+
14+
type AuditLogParams = {
15+
dynamoClient?: DynamoDBClient;
16+
entry: AuditLogEntry;
17+
};
18+
19+
const RETENTION_DAYS = 365;
20+
21+
export async function createAuditLogEntry({
22+
dynamoClient,
23+
entry,
24+
}: AuditLogParams) {
25+
const baseNow = Date.now();
26+
const timestamp = Math.floor(baseNow / 1000);
27+
const expireAt =
28+
timestamp + Math.floor((RETENTION_DAYS * 24 * 60 * 60 * 1000) / 1000);
29+
if (!dynamoClient) {
30+
dynamoClient = new DynamoDBClient({
31+
region: genericConfig.AwsRegion,
32+
});
33+
}
34+
const augmentedEntry = marshall({ ...entry, createdAt: timestamp, expireAt });
35+
const command = new PutItemCommand({
36+
TableName: genericConfig.AuditLogTable,
37+
Item: augmentedEntry,
38+
});
39+
return dynamoClient.send(command);
40+
}

src/api/functions/mobileWallet.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { promises as fs } from "fs";
1717
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
1818
import { RunEnvironment } from "common/roles.js";
1919
import pino from "pino";
20+
import { createAuditLogEntry } from "./auditLog.js";
21+
import { Modules } from "common/modules.js";
2022

2123
function trim(s: string) {
2224
return (s || "").replace(/^\s+|\s+$/g, "");
@@ -66,7 +68,6 @@ export async function issueAppleWalletMembershipCard(
6668
"base64",
6769
).toString("utf-8");
6870
pass["passTypeIdentifier"] = environmentConfig["PasskitIdentifier"];
69-
7071
const pkpass = new PKPass(
7172
{
7273
"icon.png": await fs.readFile(icon),
@@ -117,9 +118,13 @@ export async function issueAppleWalletMembershipCard(
117118
pkpass.backFields.push({ label: "Pass Created On", key: "iat", value: iat });
118119
pkpass.backFields.push({ label: "Membership ID", key: "id", value: email });
119120
const buffer = pkpass.getAsBuffer();
120-
logger.info(
121-
{ type: "audit", module: "mobileWallet", actor: initiator, target: email },
122-
"Created membership verification pass",
123-
);
121+
await createAuditLogEntry({
122+
entry: {
123+
module: Modules.MOBILE_WALLET,
124+
actor: initiator,
125+
target: email,
126+
message: "Created membership verification pass",
127+
},
128+
});
124129
return buffer;
125130
}

src/api/routes/events.ts

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
deleteCacheCounter,
3131
getCacheCounter,
3232
} from "api/functions/cache.js";
33+
import { createAuditLogEntry } from "api/functions/auditLog.js";
34+
import { Modules } from "common/modules.js";
3335

3436
const repeatOptions = ["weekly", "biweekly"] as const;
3537
export const CLIENT_HTTP_CACHE_POLICY = `public, max-age=${EVENT_CACHED_DURATION}, stale-while-revalidate=420, stale-if-error=3600`;
@@ -266,6 +268,10 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
266268
}
267269
originalEvent = unmarshall(originalEvent);
268270
}
271+
let verb = "created";
272+
if (userProvidedId && userProvidedId === entryUUID) {
273+
verb = "modified";
274+
}
269275
const entry = {
270276
...request.body,
271277
id: entryUUID,
@@ -281,10 +287,6 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
281287
Item: marshall(entry),
282288
}),
283289
);
284-
let verb = "created";
285-
if (userProvidedId && userProvidedId === entryUUID) {
286-
verb = "modified";
287-
}
288290
try {
289291
if (request.body.featured && !request.body.repeats) {
290292
await updateDiscord(
@@ -332,19 +334,20 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
332334
1,
333335
false,
334336
);
337+
await createAuditLogEntry({
338+
dynamoClient: fastify.dynamoClient,
339+
entry: {
340+
module: Modules.EVENTS,
341+
actor: request.username,
342+
target: entryUUID,
343+
message: `${verb} event "${entryUUID}"`,
344+
requestId: request.id,
345+
},
346+
});
335347
reply.status(201).send({
336348
id: entryUUID,
337349
resource: `/api/v1/events/${entryUUID}`,
338350
});
339-
request.log.info(
340-
{
341-
type: "audit",
342-
module: "events",
343-
actor: request.username,
344-
target: entryUUID,
345-
},
346-
`${verb} event "${entryUUID}"`,
347-
);
348351
} catch (e: unknown) {
349352
if (e instanceof Error) {
350353
request.log.error("Failed to insert to DynamoDB: " + e.toString());
@@ -391,6 +394,16 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
391394
id,
392395
resource: `/api/v1/events/${id}`,
393396
});
397+
await createAuditLogEntry({
398+
dynamoClient: fastify.dynamoClient,
399+
entry: {
400+
module: Modules.EVENTS,
401+
actor: request.username,
402+
target: id,
403+
message: `Deleted event "${id}"`,
404+
requestId: request.id,
405+
},
406+
});
394407
} catch (e: unknown) {
395408
if (e instanceof Error) {
396409
request.log.error("Failed to delete from DynamoDB: " + e.toString());
@@ -406,15 +419,6 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
406419
1,
407420
false,
408421
);
409-
request.log.info(
410-
{
411-
type: "audit",
412-
module: "events",
413-
actor: request.username,
414-
target: id,
415-
},
416-
`deleted event "${id}"`,
417-
);
418422
},
419423
);
420424
fastify.get<EventGetRequest>(

0 commit comments

Comments
 (0)