Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/iam/iam-policy.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function enrichWithResourceData(
return log.iamAction === denial.iamAction && timeDiff <= 5000;
});

if (nearbyResourceLogs.length > 0) {
if (!enrichedDenial.iamResource && nearbyResourceLogs.length > 0) {
enrichedDenial.iamResource = nearbyResourceLogs[0].iamResource;
}
}
Expand Down
55 changes: 55 additions & 0 deletions src/lib/logs/log-retriever.iam.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { LocalStackLogRetriever } from "./log-retriever";

describe("LocalStackLogRetriever IAM denial parsing", () => {
const parse = (line: string) => (new LocalStackLogRetriever() as any).parseLogLine(line);

it("parses the legacy 'Request for service ... denied.' format", () => {
const entry = parse(
"2025-07-23T10:58:58.710 INFO --- [asgi_gw_1] l.p.c.s.i.p.handler : Request for service 's3' by principal 'arn:aws:iam::000000000000:user/test' for operation 'CreateBucket' denied."
);
expect(entry.isIamDenial).toBe(true);
expect(entry.iamPrincipal).toBe("arn:aws:iam::000000000000:user/test");
expect(entry.iamAction).toBe("s3:CreateBucket");
});

it("parses the Explainable IAM 'is not authorized to perform' format (ENFORCED and SOFT_MODE)", () => {
const entry = parse(
"2026-10-05T15:16:27.431 INFO --- [et.reactor-2] l.p.c.s.i.p.handler : User: arn:aws:iam::000000000000:user/probe-user is not authorized to perform: dynamodb:Scan on resource: arn:aws:dynamodb:us-east-1:000000000000:table/probe-table because no identity-based policy allows the dynamodb:Scan action"
);
expect(entry.isIamDenial).toBe(true);
expect(entry.isError).toBe(true);
expect(entry.service).toBe("dynamodb");
expect(entry.iamPrincipal).toBe("arn:aws:iam::000000000000:user/probe-user");
expect(entry.iamAction).toBe("dynamodb:Scan");
expect(entry.iamResource).toBe("arn:aws:dynamodb:us-east-1:000000000000:table/probe-table");
});

it("parses assumed-role principals and object-level S3 resources", () => {
const entry = parse(
"2026-10-05T15:16:27.039 INFO --- [et.reactor-1] l.p.c.s.i.p.handler : User: arn:aws:sts::000000000000:assumed-role/my-fn-role/my-fn is not authorized to perform: s3:PutObject on resource: arn:aws:s3:::probe-bucket/hello.txt because no identity-based policy allows the s3:PutObject action"
);
expect(entry.isIamDenial).toBe(true);
expect(entry.iamPrincipal).toBe("arn:aws:sts::000000000000:assumed-role/my-fn-role/my-fn");
expect(entry.iamAction).toBe("s3:PutObject");
expect(entry.iamResource).toBe("arn:aws:s3:::probe-bucket/hello.txt");
});

it("parses denials for actions that have no resource ARN", () => {
const entry = parse(
"2026-10-06T08:33:16.832 INFO --- [et.reactor-1] l.p.c.s.i.p.handler : User: arn:aws:sts::000000000000:assumed-role/nd-role/nd is not authorized to perform: dynamodb:ListTables because no identity-based policy allows the dynamodb:ListTables action"
);
expect(entry.isIamDenial).toBe(true);
expect(entry.iamPrincipal).toBe("arn:aws:sts::000000000000:assumed-role/nd-role/nd");
expect(entry.iamAction).toBe("dynamodb:ListTables");
expect(entry.iamResource).toBeUndefined();
});

it("does not flag the DEBUG 'Necessary permissions' lines as denials but still extracts the resource", () => {
const entry = parse(
"2026-10-05T15:16:27.431 DEBUG --- [et.reactor-2] l.p.c.s.i.p.handler : Necessary permissions for this action: [\"Action 'dynamodb:Scan' for 'arn:aws:dynamodb:us-east-1:000000000000:table/probe-table'\"]"
);
expect(entry.isIamDenial).toBeUndefined();
expect(entry.iamAction).toBe("dynamodb:Scan");
expect(entry.iamResource).toBe("arn:aws:dynamodb:us-east-1:000000000000:table/probe-table");
});
});
17 changes: 17 additions & 0 deletions src/lib/logs/log-retriever.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,23 @@ export class LocalStackLogRetriever {
entry.isError = true;
}

// Detect the newer Explainable IAM denial format
if (!entry.isIamDenial) {
const explainableDenialPattern =
/User: (\S+) is not authorized to perform: ([A-Za-z0-9-]+:[A-Za-z0-9*]+)(?: on resource: "?([^"\s]+)"?)? because/;
const explainableMatch = line.match(explainableDenialPattern);
if (explainableMatch) {
entry.isIamDenial = true;
entry.iamPrincipal = explainableMatch[1];
entry.iamAction = explainableMatch[2];
if (explainableMatch[3]) {
entry.iamResource = explainableMatch[3];
}
entry.service = explainableMatch[2].split(":")[0].toLowerCase();
entry.isError = true;
}
}

const iamResourcePattern = /Action '([^']*)' for '([^']*)'/g;
let resourceMatch;
while ((resourceMatch = iamResourcePattern.exec(line)) !== null) {
Expand Down
Loading