@intentius/chant-lexicon-aws 0.44.14 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentcore/trace-fetch.d.ts +4 -1
- package/dist/agentcore/trace-fetch.d.ts.map +1 -1
- package/dist/api/read-client.d.ts +31 -1
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/cloud-executor.d.ts +9 -0
- package/dist/components/cloud-executor.d.ts.map +1 -1
- package/dist/composites/agentcore-agent.d.ts +33 -19
- package/dist/composites/agentcore-agent.d.ts.map +1 -1
- package/dist/composites/index.d.ts +1 -1
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/lambda-function.d.ts +4 -4
- package/dist/composites/lambda-function.d.ts.map +1 -1
- package/dist/deep-observe.d.ts +21 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +5 -3
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/cf-refs.d.ts +7 -0
- package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/waw059.d.ts +36 -0
- package/dist/lint/post-synth/waw059.d.ts.map +1 -0
- package/dist/lint/post-synth/waw060.d.ts +16 -0
- package/dist/lint/post-synth/waw060.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +2 -0
- package/dist/okf/rules/WAW059.md +25 -0
- package/dist/okf/rules/WAW060.md +17 -0
- package/dist/okf/types/Action.md +1 -0
- package/dist/okf/types/Bucket.md +1 -0
- package/dist/okf/types/GlobalTable.md +4 -0
- package/dist/okf/types/IamPolicy.md +2 -0
- package/dist/okf/types/InstanceProfile.md +4 -0
- package/dist/okf/types/ManagedPolicy.md +2 -0
- package/dist/okf/types/Map.md +1 -0
- package/dist/okf/types/Queue.md +1 -0
- package/dist/okf/types/Role.md +1 -0
- package/dist/okf/types/Table.md +1 -0
- package/dist/okf/types/Type.md +2 -0
- package/dist/op/activities/aws-apply.d.ts +62 -9
- package/dist/op/activities/aws-apply.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +5 -4
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/ownership.d.ts +18 -0
- package/dist/ownership.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/properties.d.ts +4 -3
- package/dist/properties.d.ts.map +1 -1
- package/dist/rules/cf-refs.ts +22 -0
- package/dist/rules/waw059.ts +353 -0
- package/dist/rules/waw060.ts +91 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/spec/fetch.d.ts +12 -1
- package/dist/spec/fetch.d.ts.map +1 -1
- package/dist/teardown.d.ts +85 -0
- package/dist/teardown.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/agentcore/trace-fetch.test.ts +17 -0
- package/src/agentcore/trace-fetch.ts +7 -2
- package/src/api/read-client.test.ts +87 -0
- package/src/api/read-client.ts +57 -1
- package/src/codegen/docs-links.test.ts +44 -30
- package/src/codegen/docs.ts +2 -1034
- package/src/components/capability-plugin.ts +5 -2
- package/src/components/cloud-executor.test.ts +29 -1
- package/src/components/cloud-executor.ts +33 -6
- package/src/composites/agentcore-agent.test.ts +32 -12
- package/src/composites/agentcore-agent.ts +43 -23
- package/src/composites/index.ts +1 -1
- package/src/composites/microvm-app.test.ts +2 -2
- package/src/deep-observe.test.ts +94 -0
- package/src/deep-observe.ts +58 -2
- package/src/import/roundtrip-fixtures.test.ts +1 -1
- package/src/index.ts +3 -1
- package/src/lifecycle-integration.test.ts +93 -0
- package/src/lint/audit-catalog.ts +5 -0
- package/src/lint/post-synth/cf-refs.ts +22 -0
- package/src/lint/post-synth/index.ts +4 -0
- package/src/lint/post-synth/waw059.test.ts +309 -0
- package/src/lint/post-synth/waw059.ts +353 -0
- package/src/lint/post-synth/waw060.test.ts +131 -0
- package/src/lint/post-synth/waw060.ts +91 -0
- package/src/op/activities/aws-apply.test.ts +151 -4
- package/src/op/activities/aws-apply.ts +123 -13
- package/src/op/activities/index.ts +5 -3
- package/src/ownership.test.ts +24 -1
- package/src/ownership.ts +37 -0
- package/src/plugin.ts +52 -35
- package/src/properties.test.ts +5 -5
- package/src/properties.ts +6 -5
- package/src/serializer-ownership.test.ts +18 -0
- package/src/serializer.test.ts +73 -33
- package/src/serializer.ts +9 -1
- package/src/spec/fetch.test.ts +40 -0
- package/src/spec/fetch.ts +24 -3
- package/src/teardown.test.ts +258 -0
- package/src/teardown.ts +276 -0
|
@@ -28,8 +28,11 @@ const { awsPlugin } = await import("./plugin");
|
|
|
28
28
|
const { liveImportFromPlugins } = await import("@intentius/chant/cli/commands/import");
|
|
29
29
|
const { buildChangeSet } = await import("@intentius/chant/lifecycle/change-set");
|
|
30
30
|
const { normalizeObservation } = await import("@intentius/chant/observation");
|
|
31
|
+
const { buildLiveGraphIr } = await import("@intentius/chant/graph-ir");
|
|
31
32
|
const { liveEvidenceFromChangeSet, reconcileStatus } = await import("@intentius/chant/lifecycle/status");
|
|
32
33
|
const { describeObservationConformance } = await import("@intentius/chant-test-utils");
|
|
34
|
+
const { observeResources } = await import("@intentius/chant/lifecycle/observe");
|
|
35
|
+
const { DECLARABLE_MARKER } = await import("@intentius/chant/declarable");
|
|
33
36
|
|
|
34
37
|
const liveTemplate = {
|
|
35
38
|
AWSTemplateFormatVersion: "2010-09-09",
|
|
@@ -143,6 +146,49 @@ describe("aws lifecycle integration (#163)", () => {
|
|
|
143
146
|
expect(cs2.entries.find((e) => e.name === "MyBucket")!.action).toBe("noop");
|
|
144
147
|
});
|
|
145
148
|
|
|
149
|
+
// #1279 — a node's attributes are the resource's own. The stack's outputs
|
|
150
|
+
// used to be copied onto every member, so a VPC carried `expWebIp` and no
|
|
151
|
+
// `CidrBlock`; they now ride the envelope once, keyed by the stack.
|
|
152
|
+
test("an observed VPC carries its own properties; the stack carries the exports (#1279)", async () => {
|
|
153
|
+
stubCfn((action) =>
|
|
154
|
+
action === "DescribeStackResources"
|
|
155
|
+
? { text: stackResourcesXml([{ logicalId: "vpc", type: "AWS::EC2::VPC", physicalId: "vpc-a5a41663" }]) }
|
|
156
|
+
: { text: stackOutputsXml({ expVpcId: "vpc-a5a41663", expWebIp: "54.1.2.3", expDbPassword: "hunter2" }) },
|
|
157
|
+
);
|
|
158
|
+
spawnMock.mockResolvedValue(
|
|
159
|
+
ok(JSON.stringify({ Vpcs: [{ VpcId: "vpc-a5a41663", CidrBlock: "10.0.0.0/16", IsDefault: false }] })),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const observed = normalizeObservation(
|
|
163
|
+
await awsPlugin.describeResources!({
|
|
164
|
+
environment: "prod",
|
|
165
|
+
buildOutput: "",
|
|
166
|
+
entityNames: ["vpc"],
|
|
167
|
+
entities: new Map(),
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const attrs = observed.resources.vpc?.attributes ?? {};
|
|
172
|
+
expect(attrs.CidrBlock).toBe("10.0.0.0/16");
|
|
173
|
+
expect(attrs.IsDefault).toBe(false);
|
|
174
|
+
expect(Object.keys(attrs).filter((k) => k.startsWith("exp"))).toEqual([]);
|
|
175
|
+
|
|
176
|
+
expect(observed.stackExports).toEqual({
|
|
177
|
+
prod: { expVpcId: "vpc-a5a41663", expWebIp: "54.1.2.3", expDbPassword: "[REDACTED]" },
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// And through the graph: the exports land on the IR's `exports`, not on the node.
|
|
181
|
+
const ir = buildLiveGraphIr([{ lexicon: "aws", resources: observed.resources, stackExports: observed.stackExports }]);
|
|
182
|
+
const node = ir.nodes.find((n) => n.id === "vpc")!;
|
|
183
|
+
expect(node.attrs.CidrBlock).toBe("10.0.0.0/16");
|
|
184
|
+
expect(node.attrs.expVpcId).toBeUndefined();
|
|
185
|
+
expect(ir.exports).toEqual([
|
|
186
|
+
{ name: "expDbPassword", value: "[REDACTED]", stack: "prod" },
|
|
187
|
+
{ name: "expVpcId", value: "vpc-a5a41663", stack: "prod" },
|
|
188
|
+
{ name: "expWebIp", value: "54.1.2.3", stack: "prod" },
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
|
|
146
192
|
// #1647 — the carve state, end to end: terraform applied the bucket, carve
|
|
147
193
|
// emitted a carveout declaring it by BucketName, and no CFN stack has ever
|
|
148
194
|
// heard of it. The stack read alone said confirmed-absent (missing → a plan
|
|
@@ -303,6 +349,49 @@ describe("aws lifecycle integration (#163)", () => {
|
|
|
303
349
|
});
|
|
304
350
|
expect(cs.entries[0].action).toBe("create");
|
|
305
351
|
});
|
|
352
|
+
|
|
353
|
+
// #1265 — the ownership notice printed once per `describeResources` call, so
|
|
354
|
+
// a four-stack project got four identical copies ahead of every answer. It
|
|
355
|
+
// is a property of the read path, not of a stack: the plugin now returns it
|
|
356
|
+
// as a note on the observation and core says it once per run.
|
|
357
|
+
test("owned read on a four-stack project: one ownership note, nothing printed", async () => {
|
|
358
|
+
stubCfn((action) =>
|
|
359
|
+
action === "DescribeStackResources"
|
|
360
|
+
? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
|
|
361
|
+
: { text: stackOutputsXml() },
|
|
362
|
+
);
|
|
363
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
364
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
365
|
+
const entities = new Map([
|
|
366
|
+
["MyBucket", { [DECLARABLE_MARKER]: true as const, lexicon: "aws", entityType: "AWS::S3::Bucket", props: {} }],
|
|
367
|
+
]);
|
|
368
|
+
const result = await observeResources(
|
|
369
|
+
"prod",
|
|
370
|
+
[awsPlugin],
|
|
371
|
+
{ errors: [], warnings: [], entities, outputs: new Map([["aws", ""]]) } as never,
|
|
372
|
+
{ owned: true, stacks: ["prod-net", "prod-data", "prod-app", "prod-edge"] },
|
|
373
|
+
);
|
|
374
|
+
const ownership = result.notes.filter((n) => n.includes("ownership filter unavailable"));
|
|
375
|
+
expect(ownership).toHaveLength(1);
|
|
376
|
+
expect(ownership[0]).toBe(
|
|
377
|
+
"[aws] ownership filter unavailable on describeResources (no tags from describe-stack-resources) — returning all, each with an explicit `unknown` verdict; use `chant import --from <env> --owned` for ownership-filtered export",
|
|
378
|
+
);
|
|
379
|
+
expect(result.warnings.join("\n")).not.toContain("ownership filter unavailable");
|
|
380
|
+
expect(warn).not.toHaveBeenCalled();
|
|
381
|
+
expect(error).not.toHaveBeenCalled();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("an unowned read carries no ownership note", async () => {
|
|
385
|
+
stubCfn((action) =>
|
|
386
|
+
action === "DescribeStackResources"
|
|
387
|
+
? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
|
|
388
|
+
: { text: stackOutputsXml() },
|
|
389
|
+
);
|
|
390
|
+
const observed = normalizeObservation(
|
|
391
|
+
await awsPlugin.describeResources!({ environment: "prod", buildOutput: "", entityNames: ["MyBucket"], entities: new Map() }),
|
|
392
|
+
);
|
|
393
|
+
expect(observed.notes).toEqual([]);
|
|
394
|
+
});
|
|
306
395
|
});
|
|
307
396
|
|
|
308
397
|
// The shared conformance suite (#1089).
|
|
@@ -339,9 +428,13 @@ describeObservationConformance({
|
|
|
339
428
|
},
|
|
340
429
|
},
|
|
341
430
|
{
|
|
431
|
+
// describe-stack-resources also carries no tags to read a stack/env
|
|
432
|
+
// identity from, so `marker` (#1222) stays absent here — stack-level
|
|
433
|
+
// handling reads the stack's own tags instead.
|
|
342
434
|
name: "a healthy stack read",
|
|
343
435
|
declared: ["MyBucket"],
|
|
344
436
|
expectPresent: ["MyBucket"],
|
|
437
|
+
expectNoMarker: ["MyBucket"],
|
|
345
438
|
run: () => {
|
|
346
439
|
stubCfn((action) =>
|
|
347
440
|
action === "DescribeStackResources"
|
|
@@ -67,4 +67,9 @@ export const awsAuditCatalog: Record<string, RuleMeta> = {
|
|
|
67
67
|
WAW056: auditRule("WAW056", "merge-worthy", "guidance", "SCP guardrail has no Deny statement", "Add a Deny statement — SCPs only filter permissions, so a Deny-less SCP guards nothing.", { authority: [AWS_SEC] }),
|
|
68
68
|
WAW057: auditRule("WAW057", "merge-worthy", "guidance", "SCP guardrail attached to no targets", "Attach the SCP to the organization root or an OU via TargetIds.", { authority: [AWS_SEC] }),
|
|
69
69
|
WAW058: auditRule("WAW058", "merge-worthy", "guidance", "Organization audit trail dropped or scoped down", "Keep an organization CloudTrail with IsLogging: true and IsMultiRegionTrail: true.", { authority: [AWS_SEC] }),
|
|
70
|
+
|
|
71
|
+
// #1225 — least-privilege tightening the declared graph can prove. Report-only,
|
|
72
|
+
// so no authority citation (those are reserved for merge-worthy entries).
|
|
73
|
+
WAW059: auditRule("WAW059", "report-only", "guidance", "Wildcard Resource where the declared graph enumerates the touched set", "Tighten Resource from \"*\" to the Fn::GetAtt Arn list of the declared resources the role's consumers touch.", { category: "security" }),
|
|
74
|
+
WAW060: auditRule("WAW060", "report-only", "guidance", "IAM policy attached to no principal", "Attach the policy via Roles/Users/Groups or reference it from a principal's ManagedPolicyArns — unattached it grants nothing.", { category: "security" }),
|
|
70
75
|
};
|
|
@@ -50,6 +50,28 @@ export function findResourceRefs(value: unknown): Set<string> {
|
|
|
50
50
|
return refs;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Build the reverse of `findResourceRefs` for a whole template: logical id →
|
|
55
|
+
* the set of logical ids whose Properties reference it via Ref/Fn::GetAtt.
|
|
56
|
+
* Lets a check walk the graph consumer-ward (who uses this role?) instead of
|
|
57
|
+
* dependency-ward. Used by WAW059.
|
|
58
|
+
*/
|
|
59
|
+
export function buildReverseRefIndex(template: CFTemplate): Map<string, Set<string>> {
|
|
60
|
+
const index = new Map<string, Set<string>>();
|
|
61
|
+
for (const [logicalId, resource] of Object.entries(template.Resources ?? {})) {
|
|
62
|
+
for (const target of findResourceRefs(resource.Properties)) {
|
|
63
|
+
if (target === logicalId) continue;
|
|
64
|
+
let consumers = index.get(target);
|
|
65
|
+
if (!consumers) {
|
|
66
|
+
consumers = new Set();
|
|
67
|
+
index.set(target, consumers);
|
|
68
|
+
}
|
|
69
|
+
consumers.add(logicalId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return index;
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
/**
|
|
54
76
|
* Check if a value is a CloudFormation intrinsic function (Ref, Fn::*, etc.)
|
|
55
77
|
* that cannot be statically evaluated.
|
|
@@ -50,6 +50,8 @@ import { waw055 } from "./waw055";
|
|
|
50
50
|
import { waw056 } from "./waw056";
|
|
51
51
|
import { waw057 } from "./waw057";
|
|
52
52
|
import { waw058 } from "./waw058";
|
|
53
|
+
import { waw059 } from "./waw059";
|
|
54
|
+
import { waw060 } from "./waw060";
|
|
53
55
|
|
|
54
56
|
export const postSynthChecks: PostSynthCheck[] = [
|
|
55
57
|
cor020,
|
|
@@ -102,4 +104,6 @@ export const postSynthChecks: PostSynthCheck[] = [
|
|
|
102
104
|
waw056,
|
|
103
105
|
waw057,
|
|
104
106
|
waw058,
|
|
107
|
+
waw059,
|
|
108
|
+
waw060,
|
|
105
109
|
];
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw059, checkWildcardResourceEnumerable } from "./waw059";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const TRUST = {
|
|
10
|
+
Version: "2012-10-17",
|
|
11
|
+
Statement: [
|
|
12
|
+
{
|
|
13
|
+
Effect: "Allow",
|
|
14
|
+
Principal: { Service: "lambda.amazonaws.com" },
|
|
15
|
+
Action: "sts:AssumeRole",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function role(statement: Record<string, unknown>) {
|
|
21
|
+
return {
|
|
22
|
+
Type: "AWS::IAM::Role",
|
|
23
|
+
Properties: {
|
|
24
|
+
AssumeRolePolicyDocument: TRUST,
|
|
25
|
+
Policies: [
|
|
26
|
+
{
|
|
27
|
+
PolicyName: "app",
|
|
28
|
+
PolicyDocument: { Version: "2012-10-17", Statement: [statement] },
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function lambda(env: Record<string, unknown>) {
|
|
36
|
+
return {
|
|
37
|
+
Type: "AWS::Lambda::Function",
|
|
38
|
+
Properties: {
|
|
39
|
+
Runtime: "nodejs20.x",
|
|
40
|
+
Handler: "index.handler",
|
|
41
|
+
Role: { "Fn::GetAtt": ["AppRole", "Arn"] },
|
|
42
|
+
Environment: { Variables: env },
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const BUCKET = { Type: "AWS::S3::Bucket", Properties: {} };
|
|
48
|
+
const TABLE = { Type: "AWS::DynamoDB::Table", Properties: { BillingMode: "PAY_PER_REQUEST" } };
|
|
49
|
+
const QUEUE = { Type: "AWS::SQS::Queue", Properties: {} };
|
|
50
|
+
|
|
51
|
+
describe("WAW059: wildcard Resource where the declared graph enumerates the touched set", () => {
|
|
52
|
+
test("check metadata", () => {
|
|
53
|
+
expect(waw059.id).toBe("WAW059");
|
|
54
|
+
expect(waw059.description).toContain("declared graph");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("flags an over-broad role inline statement and suggests the declared Arns", () => {
|
|
58
|
+
const ctx = makeCtx({
|
|
59
|
+
Resources: {
|
|
60
|
+
DataBucket: BUCKET,
|
|
61
|
+
AppRole: role({
|
|
62
|
+
Effect: "Allow",
|
|
63
|
+
Action: ["s3:GetObject", "s3:PutObject"],
|
|
64
|
+
Resource: "*",
|
|
65
|
+
}),
|
|
66
|
+
AppFunc: lambda({ BUCKET_NAME: { Ref: "DataBucket" } }),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const diags = checkWildcardResourceEnumerable(ctx);
|
|
70
|
+
expect(diags).toHaveLength(1);
|
|
71
|
+
expect(diags[0].checkId).toBe("WAW059");
|
|
72
|
+
expect(diags[0].severity).toBe("warning");
|
|
73
|
+
expect(diags[0].entity).toBe("AppRole");
|
|
74
|
+
expect(diags[0].message).toContain('{"Fn::GetAtt":["DataBucket","Arn"]}');
|
|
75
|
+
expect(diags[0].message).toContain('"/*"');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("flags a service-wide ARN pattern, not just the bare wildcard", () => {
|
|
79
|
+
const ctx = makeCtx({
|
|
80
|
+
Resources: {
|
|
81
|
+
Jobs: TABLE,
|
|
82
|
+
AppRole: role({
|
|
83
|
+
Effect: "Allow",
|
|
84
|
+
Action: ["dynamodb:GetItem", "dynamodb:Query"],
|
|
85
|
+
Resource: "arn:aws:dynamodb:*:*:table/*",
|
|
86
|
+
}),
|
|
87
|
+
AppFunc: lambda({ TABLE_NAME: { Ref: "Jobs" } }),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const diags = checkWildcardResourceEnumerable(ctx);
|
|
91
|
+
expect(diags).toHaveLength(1);
|
|
92
|
+
expect(diags[0].message).toContain('{"Fn::GetAtt":["Jobs","Arn"]}');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("flags an IAM::Policy attached to a declared role via Ref", () => {
|
|
96
|
+
const ctx = makeCtx({
|
|
97
|
+
Resources: {
|
|
98
|
+
WorkQueue: QUEUE,
|
|
99
|
+
AppRole: { Type: "AWS::IAM::Role", Properties: { AssumeRolePolicyDocument: TRUST } },
|
|
100
|
+
AppPolicy: {
|
|
101
|
+
Type: "AWS::IAM::Policy",
|
|
102
|
+
Properties: {
|
|
103
|
+
PolicyName: "app",
|
|
104
|
+
Roles: [{ Ref: "AppRole" }],
|
|
105
|
+
PolicyDocument: {
|
|
106
|
+
Version: "2012-10-17",
|
|
107
|
+
Statement: [{ Effect: "Allow", Action: "sqs:SendMessage", Resource: "*" }],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
AppFunc: lambda({ QUEUE_URL: { Ref: "WorkQueue" } }),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
const diags = checkWildcardResourceEnumerable(ctx);
|
|
115
|
+
expect(diags).toHaveLength(1);
|
|
116
|
+
expect(diags[0].entity).toBe("AppPolicy");
|
|
117
|
+
expect(diags[0].message).toContain('{"Fn::GetAtt":["WorkQueue","Arn"]}');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("exact-scoped statement is quiet", () => {
|
|
121
|
+
const ctx = makeCtx({
|
|
122
|
+
Resources: {
|
|
123
|
+
DataBucket: BUCKET,
|
|
124
|
+
AppRole: role({
|
|
125
|
+
Effect: "Allow",
|
|
126
|
+
Action: ["s3:GetObject"],
|
|
127
|
+
Resource: [{ "Fn::GetAtt": ["DataBucket", "Arn"] }],
|
|
128
|
+
}),
|
|
129
|
+
AppFunc: lambda({ BUCKET_NAME: { Ref: "DataBucket" } }),
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
expect(checkWildcardResourceEnumerable(ctx)).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("foreign edge (literal service ARN in a consumer) is quiet", () => {
|
|
136
|
+
const ctx = makeCtx({
|
|
137
|
+
Resources: {
|
|
138
|
+
DataBucket: BUCKET,
|
|
139
|
+
AppRole: role({ Effect: "Allow", Action: "s3:GetObject", Resource: "*" }),
|
|
140
|
+
AppFunc: lambda({
|
|
141
|
+
BUCKET_NAME: { Ref: "DataBucket" },
|
|
142
|
+
EXTERNAL_BUCKET: "arn:aws:s3:::partner-drop-zone",
|
|
143
|
+
}),
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
expect(checkWildcardResourceEnumerable(ctx)).toHaveLength(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("intrinsic edge (Fn::ImportValue in a consumer) is quiet", () => {
|
|
150
|
+
const ctx = makeCtx({
|
|
151
|
+
Resources: {
|
|
152
|
+
DataBucket: BUCKET,
|
|
153
|
+
AppRole: role({ Effect: "Allow", Action: "s3:GetObject", Resource: "*" }),
|
|
154
|
+
AppFunc: lambda({
|
|
155
|
+
BUCKET_NAME: { Ref: "DataBucket" },
|
|
156
|
+
SHARED_BUCKET: { "Fn::ImportValue": "shared-bucket-name" },
|
|
157
|
+
}),
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
expect(checkWildcardResourceEnumerable(ctx)).toHaveLength(0);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("statement with a Condition is quiet", () => {
|
|
164
|
+
const ctx = makeCtx({
|
|
165
|
+
Resources: {
|
|
166
|
+
DataBucket: BUCKET,
|
|
167
|
+
AppRole: role({
|
|
168
|
+
Effect: "Allow",
|
|
169
|
+
Action: "s3:GetObject",
|
|
170
|
+
Resource: "*",
|
|
171
|
+
Condition: { StringEquals: { "aws:PrincipalOrgID": "o-abc123" } },
|
|
172
|
+
}),
|
|
173
|
+
AppFunc: lambda({ BUCKET_NAME: { Ref: "DataBucket" } }),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
expect(checkWildcardResourceEnumerable(ctx)).toHaveLength(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("action outside the curated table is quiet", () => {
|
|
180
|
+
const ctx = makeCtx({
|
|
181
|
+
Resources: {
|
|
182
|
+
DataBucket: BUCKET,
|
|
183
|
+
AppRole: role({
|
|
184
|
+
Effect: "Allow",
|
|
185
|
+
Action: ["ec2:DescribeInstances", "elasticloadbalancing:DescribeTargetGroups"],
|
|
186
|
+
Resource: "*",
|
|
187
|
+
}),
|
|
188
|
+
AppFunc: lambda({ BUCKET_NAME: { Ref: "DataBucket" } }),
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
expect(checkWildcardResourceEnumerable(ctx)).toHaveLength(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("action needing '*' in a curated service is quiet (not in the table)", () => {
|
|
195
|
+
const ctx = makeCtx({
|
|
196
|
+
Resources: {
|
|
197
|
+
DataBucket: BUCKET,
|
|
198
|
+
AppRole: role({ Effect: "Allow", Action: "s3:ListAllMyBuckets", Resource: "*" }),
|
|
199
|
+
AppFunc: lambda({ BUCKET_NAME: { Ref: "DataBucket" } }),
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
expect(checkWildcardResourceEnumerable(ctx)).toHaveLength(0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("role with no consumers is quiet", () => {
|
|
206
|
+
const ctx = makeCtx({
|
|
207
|
+
Resources: {
|
|
208
|
+
DataBucket: BUCKET,
|
|
209
|
+
AppRole: role({ Effect: "Allow", Action: "s3:GetObject", Resource: "*" }),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
expect(checkWildcardResourceEnumerable(ctx)).toHaveLength(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("consumers that touch no declared resource of the service are quiet", () => {
|
|
216
|
+
const ctx = makeCtx({
|
|
217
|
+
Resources: {
|
|
218
|
+
AppRole: role({ Effect: "Allow", Action: "s3:GetObject", Resource: "*" }),
|
|
219
|
+
AppFunc: lambda({ STAGE: "prod" }),
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
expect(checkWildcardResourceEnumerable(ctx)).toHaveLength(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("trust policy statements never fire (Principal gate)", () => {
|
|
226
|
+
const ctx = makeCtx({
|
|
227
|
+
Resources: {
|
|
228
|
+
DataBucket: BUCKET,
|
|
229
|
+
AppRole: { Type: "AWS::IAM::Role", Properties: { AssumeRolePolicyDocument: TRUST } },
|
|
230
|
+
AppFunc: lambda({ BUCKET_NAME: { Ref: "DataBucket" } }),
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
expect(checkWildcardResourceEnumerable(ctx)).toHaveLength(0);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("policy attached to users or literal role names is quiet", () => {
|
|
237
|
+
const ctx = makeCtx({
|
|
238
|
+
Resources: {
|
|
239
|
+
DataBucket: BUCKET,
|
|
240
|
+
UserPolicy: {
|
|
241
|
+
Type: "AWS::IAM::Policy",
|
|
242
|
+
Properties: {
|
|
243
|
+
PolicyName: "u",
|
|
244
|
+
Users: ["alice"],
|
|
245
|
+
PolicyDocument: {
|
|
246
|
+
Version: "2012-10-17",
|
|
247
|
+
Statement: [{ Effect: "Allow", Action: "s3:GetObject", Resource: "*" }],
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
NamePolicy: {
|
|
252
|
+
Type: "AWS::IAM::Policy",
|
|
253
|
+
Properties: {
|
|
254
|
+
PolicyName: "n",
|
|
255
|
+
Roles: ["pre-existing-role-name"],
|
|
256
|
+
PolicyDocument: {
|
|
257
|
+
Version: "2012-10-17",
|
|
258
|
+
Statement: [{ Effect: "Allow", Action: "s3:GetObject", Resource: "*" }],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
expect(checkWildcardResourceEnumerable(ctx)).toHaveLength(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("managed policy attached from the role side via ManagedPolicyArns fires", () => {
|
|
268
|
+
const ctx = makeCtx({
|
|
269
|
+
Resources: {
|
|
270
|
+
Jobs: TABLE,
|
|
271
|
+
AppManaged: {
|
|
272
|
+
Type: "AWS::IAM::ManagedPolicy",
|
|
273
|
+
Properties: {
|
|
274
|
+
PolicyDocument: {
|
|
275
|
+
Version: "2012-10-17",
|
|
276
|
+
Statement: [{ Effect: "Allow", Action: "dynamodb:PutItem", Resource: "*" }],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
AppRole: {
|
|
281
|
+
Type: "AWS::IAM::Role",
|
|
282
|
+
Properties: {
|
|
283
|
+
AssumeRolePolicyDocument: TRUST,
|
|
284
|
+
ManagedPolicyArns: [{ Ref: "AppManaged" }],
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
AppFunc: lambda({ TABLE_NAME: { Ref: "Jobs" } }),
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
const diags = checkWildcardResourceEnumerable(ctx);
|
|
291
|
+
expect(diags).toHaveLength(1);
|
|
292
|
+
expect(diags[0].entity).toBe("AppManaged");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("mixed wildcard and concrete Resource entries are quiet (partially scoped)", () => {
|
|
296
|
+
const ctx = makeCtx({
|
|
297
|
+
Resources: {
|
|
298
|
+
DataBucket: BUCKET,
|
|
299
|
+
AppRole: role({
|
|
300
|
+
Effect: "Allow",
|
|
301
|
+
Action: "s3:GetObject",
|
|
302
|
+
Resource: ["arn:aws:s3:::some-bucket/*", "*"],
|
|
303
|
+
}),
|
|
304
|
+
AppFunc: lambda({ BUCKET_NAME: { Ref: "DataBucket" } }),
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
expect(checkWildcardResourceEnumerable(ctx)).toHaveLength(0);
|
|
308
|
+
});
|
|
309
|
+
});
|