@intentius/chant-lexicon-aws 0.33.1 → 0.34.1

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.
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "82f137b9dc9816456d5230a0150bcdd48c79224e6fb237c3bf4951dfe6a7608b",
5
- "meta.json": "e991aef2b315b2172b66aaa916ac1ff93a13446ec08f169c6c219aa4850f965a",
6
- "types/index.d.ts": "fd13489a9f92b01b620576a283d21423b231e085a0783869c389be9a9c5d9a60",
4
+ "manifest.json": "19b9bbcad97c5b76a22524d8a05039ecbca4752d72db220091c254f0afab5380",
5
+ "meta.json": "f430880414df1288383dc91c313e50ecab26d8e7bcce520bf894eeb89b220e9f",
6
+ "types/index.d.ts": "4d6f07139586f3d7fc4d9ea0756161587709a39e21f20fbdb05e31f32a44f1c1",
7
7
  "rules/hardcoded-region.ts": "5a0eaf7ab391231fe6cd51426ece29539cb4b36f31c8dd060956638fed55722a",
8
8
  "rules/iam-wildcard.ts": "135d7217d278fef50939e605c5da32603ba35674b8c4b5c4d66a06c77903e945",
9
9
  "rules/s3-encryption.ts": "c4f9f8aa8dc382ed98c7b04e820a600590cc75335a8db2e087cbfbfe4003d09e",
@@ -59,5 +59,5 @@
59
59
  "skills/chant-aws-eks.md": "8789255709ff004ad0a875fd5999edcdc66fc6e33d710db058d9f42703bcfdfe",
60
60
  "skills/chant-aws-carve-terraform.md": "f4c6fe1c702250665f90d82b3bdaba5ea50ae75e380a8ae321dad6eb1c39683e"
61
61
  },
62
- "composite": "7b518a1dfe34d3687de1345b606674017c87c7a687ed837870c953ec24841aae"
62
+ "composite": "cdde845bc0699082a87af1b7b7558e73c1ddc0db5310eaad60aa26ce8814c41b"
63
63
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws",
3
- "version": "0.33.1",
3
+ "version": "0.34.1",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "AWS",
6
6
  "intrinsics": [
package/dist/meta.json CHANGED
@@ -44553,6 +44553,67 @@
44553
44553
  "kind": "property",
44554
44554
  "lexicon": "aws"
44555
44555
  },
44556
+ "HarnessEndpoint": {
44557
+ "resourceType": "AWS::BedrockAgentCore::HarnessEndpoint",
44558
+ "kind": "resource",
44559
+ "lexicon": "aws",
44560
+ "attrs": {
44561
+ "Arn": "Arn",
44562
+ "HarnessName": "HarnessName",
44563
+ "Status": "Status",
44564
+ "LiveVersion": "LiveVersion",
44565
+ "CreatedAt": "CreatedAt",
44566
+ "UpdatedAt": "UpdatedAt"
44567
+ },
44568
+ "propertyConstraints": {
44569
+ "HarnessId": {
44570
+ "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,39}-[a-zA-Z0-9]{10}$"
44571
+ },
44572
+ "EndpointName": {
44573
+ "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,47}$"
44574
+ },
44575
+ "TargetVersion": {
44576
+ "pattern": "^([1-9][0-9]{0,4})$",
44577
+ "minLength": 1,
44578
+ "maxLength": 5
44579
+ },
44580
+ "LiveVersion": {
44581
+ "pattern": "^([1-9][0-9]{0,4})$",
44582
+ "minLength": 1,
44583
+ "maxLength": 5
44584
+ },
44585
+ "Description": {
44586
+ "minLength": 1,
44587
+ "maxLength": 256
44588
+ },
44589
+ "CreatedAt": {
44590
+ "format": "date-time"
44591
+ },
44592
+ "UpdatedAt": {
44593
+ "format": "date-time"
44594
+ }
44595
+ },
44596
+ "createOnly": [
44597
+ "HarnessId",
44598
+ "EndpointName"
44599
+ ],
44600
+ "writeOnly": [
44601
+ "TargetVersion"
44602
+ ],
44603
+ "primaryIdentifier": [
44604
+ "Arn"
44605
+ ],
44606
+ "tagging": {
44607
+ "taggable": true,
44608
+ "tagOnCreate": true,
44609
+ "tagUpdatable": true
44610
+ }
44611
+ },
44612
+ "HarnessEndpoint_Tag": {
44613
+ "resourceType": "AWS::BedrockAgentCore::HarnessEndpoint.Tag",
44614
+ "kind": "property",
44615
+ "lexicon": "aws"
44616
+ },
44556
44617
  "HarnessEnvironmentArtifact": {
44557
44618
  "resourceType": "AWS::BedrockAgentCore::Harness.HarnessEnvironmentArtifact",
44558
44619
  "kind": "property",
@@ -70884,7 +70945,7 @@
70884
70945
  "maxLength": 256
70885
70946
  },
70886
70947
  "FunctionName": {
70887
- "pattern": "^(arn:(aws[a-zA-Z-]*)?:lambda:)?((eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?$",
70948
+ "pattern": "^(arn:(aws[a-zA-Z-]*)?:lambda:)?((eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\\$LATEST(\\.PUBLISHED)?|[a-zA-Z0-9-_]+))?$",
70888
70949
  "minLength": 1,
70889
70950
  "maxLength": 140
70890
70951
  },
@@ -91147,6 +91208,36 @@
91147
91208
  "kind": "property",
91148
91209
  "lexicon": "aws"
91149
91210
  },
91211
+ "ServiceSetting": {
91212
+ "resourceType": "AWS::SSM::ServiceSetting",
91213
+ "kind": "resource",
91214
+ "lexicon": "aws",
91215
+ "attrs": {
91216
+ "Status": "Status",
91217
+ "LastModifiedDate": "LastModifiedDate",
91218
+ "LastModifiedUser": "LastModifiedUser",
91219
+ "Arn": "Arn"
91220
+ },
91221
+ "propertyConstraints": {
91222
+ "SettingId": {
91223
+ "minLength": 1,
91224
+ "maxLength": 1000
91225
+ },
91226
+ "SettingValue": {
91227
+ "minLength": 1,
91228
+ "maxLength": 4096
91229
+ },
91230
+ "LastModifiedDate": {
91231
+ "format": "date-time"
91232
+ }
91233
+ },
91234
+ "createOnly": [
91235
+ "SettingId"
91236
+ ],
91237
+ "primaryIdentifier": [
91238
+ "Arn"
91239
+ ]
91240
+ },
91150
91241
  "ServiceSoftwareOptions": {
91151
91242
  "resourceType": "AWS::OpenSearchService::Domain.ServiceSoftwareOptions",
91152
91243
  "kind": "property",
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAyJ,MAAM,0BAA0B,CAAC;AA0BrN;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AA6DnD,eAAO,MAAM,SAAS,EAAE,aAm2BvB,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAgL,MAAM,0BAA0B,CAAC;AA6B5O;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAWnD,eAAO,MAAM,SAAS,EAAE,aAm5BvB,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * A managed resource's own properties (#1279).
3
+ *
4
+ * `describe-stack-resources` returns identity and status — logical id, physical
5
+ * id, type, timestamp — and nothing about the resource itself. So the managed
6
+ * observation filled `attributes` with the *stack's* outputs instead, copied
7
+ * onto every resource in the stack. Every node in a stack came out carrying the
8
+ * same `expVpcId`/`expWebIp` keys, and no node carried its own `VpcId`.
9
+ *
10
+ * That is invisible until something asks. `search --show VpcId` printed a blank
11
+ * column for six instances, which reads as "the estate has no VPCs" rather than
12
+ * "chant never read that". An agent asked which instances were outside the
13
+ * default VPC concluded all six were, because nothing in the graph said
14
+ * otherwise.
15
+ *
16
+ * A deep read (Cloud Control) answers this properly but is a per-resource call
17
+ * and is not available on every endpoint. This is the cheap middle: one describe
18
+ * per kind for the whole observation, joined back by physical id. Stack outputs
19
+ * are kept — they were the only attributes for a long time and queries lean on
20
+ * them — but a resource's own properties win a name collision, because they are
21
+ * the resource's.
22
+ */
23
+ import type { ResourceMetadata } from "@intentius/chant/lexicon";
24
+ /**
25
+ * Stamp the region each resource was observed in (#1279).
26
+ *
27
+ * The observation is already scoped per stack and each stack declares its
28
+ * region, so the reader knows this and was throwing it away. Without it the
29
+ * only route to "which region is this in" was parsing
30
+ * `Placement.AvailabilityZone` and trimming the last character — a trick that
31
+ * happens to work for EC2 and for nothing else. Region is a dimension of the
32
+ * estate, not a substring of an availability zone.
33
+ */
34
+ export declare function stampRegion(resources: Record<string, ResourceMetadata>, region?: string): Record<string, ResourceMetadata>;
35
+ /** True when this lexicon can read the kind's own properties. */
36
+ export declare function canDescribe(kind: string): boolean;
37
+ /**
38
+ * Merge each resource's own properties into an observation, in place of nothing.
39
+ *
40
+ * Best-effort per kind, and per kind only: an endpoint that cannot answer
41
+ * `describe-subnets` still yields instance properties, and a total failure
42
+ * leaves the observation exactly as it arrived. The managed observation is
43
+ * already complete without any of this — these are additional facts about
44
+ * resources chant has already identified, so a miss costs detail, never
45
+ * correctness.
46
+ */
47
+ export declare function describeOwnProperties(resources: Record<string, ResourceMetadata>, region?: string): Promise<Record<string, ResourceMetadata>>;
48
+ //# sourceMappingURL=properties.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"properties.d.ts","sourceRoot":"","sources":["../src/properties.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAkCjE;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAC3C,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAUlC;AAED,iEAAiE;AACjE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAC3C,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAgE3C"}
@@ -1 +1 @@
1
- {"version":3,"file":"reference-catalog.d.ts","sourceRoot":"","sources":["../src/reference-catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,EAAE,gBAoEjC,CAAC"}
1
+ {"version":3,"file":"reference-catalog.d.ts","sourceRoot":"","sources":["../src/reference-catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,EAAE,gBAyFjC,CAAC"}
@@ -16403,6 +16403,38 @@ export declare class Harness {
16403
16403
  readonly Version: string;
16404
16404
  }
16405
16405
 
16406
+ export declare class HarnessEndpoint {
16407
+ constructor(props: {
16408
+ /** The name of the endpoint. Must start with a letter and contain only alphanumeric characters and underscores. */
16409
+ EndpointName: string;
16410
+ /** The ID of the harness that the endpoint belongs to. */
16411
+ HarnessId: string;
16412
+ /** The Amazon Resource Name (ARN) of the endpoint. */
16413
+ Arn?: string;
16414
+ /** The timestamp when the endpoint was created. */
16415
+ CreatedAt?: string;
16416
+ /** The description of the endpoint. */
16417
+ Description?: string;
16418
+ /** The name of the harness that the endpoint belongs to. */
16419
+ HarnessName?: string;
16420
+ /** The harness version that the endpoint is currently serving. */
16421
+ LiveVersion?: string;
16422
+ Status?: HarnessEndpoint_HarnessEndpointStatus;
16423
+ /** Tags to apply to the harness endpoint resource. */
16424
+ Tags?: HarnessEndpoint_Tag[];
16425
+ /** The harness version that the endpoint points to and serves invocations from. */
16426
+ TargetVersion?: string;
16427
+ /** The timestamp when the endpoint was last updated. */
16428
+ UpdatedAt?: string;
16429
+ }, attributes?: CFResourceAttributes);
16430
+ readonly Arn: string;
16431
+ readonly CreatedAt: string;
16432
+ readonly HarnessName: string;
16433
+ readonly LiveVersion: string;
16434
+ readonly Status: HarnessEndpoint_HarnessEndpointStatus;
16435
+ readonly UpdatedAt: string;
16436
+ }
16437
+
16406
16438
  export declare class HealthCheck {
16407
16439
  constructor(props: {
16408
16440
  /** A complex type that contains information about the health check. */
@@ -32859,6 +32891,27 @@ export declare class ServiceProfile {
32859
32891
  readonly LoRaWAN_UlRatePolicy: string;
32860
32892
  }
32861
32893
 
32894
+ export declare class ServiceSetting {
32895
+ constructor(props: {
32896
+ /** The ID of the service setting, such as /ssm/parameter-store/high-throughput-enabled. */
32897
+ SettingId: string;
32898
+ /** The value of the service setting. */
32899
+ SettingValue: string;
32900
+ /** The ARN of the service setting. */
32901
+ Arn?: string;
32902
+ /** The last time the service setting was modified. */
32903
+ LastModifiedDate?: string;
32904
+ /** The ARN of the last modified user. */
32905
+ LastModifiedUser?: string;
32906
+ /** The status of the service setting. The value can be Default, Customized or PendingUpdate. */
32907
+ Status?: string;
32908
+ }, attributes?: CFResourceAttributes);
32909
+ readonly Arn: string;
32910
+ readonly LastModifiedDate: string;
32911
+ readonly LastModifiedUser: string;
32912
+ readonly Status: string;
32913
+ }
32914
+
32862
32915
  export declare class ServiceTemplate {
32863
32916
  constructor(props: {
32864
32917
  /** <p>The Amazon Resource Name (ARN) of the service template.</p> */
@@ -57089,7 +57142,7 @@ export declare class ContainerRecipe_EbsInstanceBlockDeviceSpecification {
57089
57142
  /** Use to override the device's volume size. */
57090
57143
  VolumeSize?: number;
57091
57144
  /** Use to override the device's volume type. */
57092
- VolumeType?: "gp2" | "gp3" | "io1" | "io2" | "sc1" | "st1" | "standard";
57145
+ VolumeType?: "gp2" | "gp3" | "gp3a" | "io1" | "io2" | "io2a" | "sc1" | "st1" | "standard";
57093
57146
  });
57094
57147
  }
57095
57148
 
@@ -75141,7 +75194,7 @@ export declare class GlobalTable_ReplicaSSESpecification {
75141
75194
 
75142
75195
  export declare class GlobalTable_ReplicaStreamSpecification {
75143
75196
  constructor(props: {
75144
- ResourcePolicy?: GlobalTable_ResourcePolicy;
75197
+ ResourcePolicy: GlobalTable_ResourcePolicy;
75145
75198
  });
75146
75199
  }
75147
75200
 
@@ -76693,6 +76746,13 @@ export declare class HarnessBedrockModelConfig {
76693
76746
  });
76694
76747
  }
76695
76748
 
76749
+ export declare class HarnessEndpoint_Tag {
76750
+ constructor(props: {
76751
+ Key: string;
76752
+ Value: string;
76753
+ });
76754
+ }
76755
+
76696
76756
  export declare class HarnessEnvironmentArtifact {
76697
76757
  constructor(props: {
76698
76758
  ContainerConfiguration?: Harness_ContainerConfiguration;
@@ -78724,7 +78784,7 @@ export declare class ImageRecipe_EbsInstanceBlockDeviceSpecification {
78724
78784
  /** Use to override the device's volume size. */
78725
78785
  VolumeSize?: number;
78726
78786
  /** Use to override the device's volume type. */
78727
- VolumeType?: "gp2" | "gp3" | "io1" | "io2" | "sc1" | "st1" | "standard";
78787
+ VolumeType?: "gp2" | "gp3" | "gp3a" | "io1" | "io2" | "io2a" | "sc1" | "st1" | "standard";
78728
78788
  });
78729
78789
  }
78730
78790
 
@@ -86435,7 +86495,9 @@ export declare class Listener_RedirectConfig {
86435
86495
 
86436
86496
  export declare class Listener_Tag {
86437
86497
  constructor(props: {
86498
+ /** The key of the tag. */
86438
86499
  Key: string;
86500
+ /** The value of the tag. */
86439
86501
  Value: string;
86440
86502
  });
86441
86503
  }
@@ -106283,7 +106345,7 @@ export declare class ReplicaSSESpecification {
106283
106345
 
106284
106346
  export declare class ReplicaStreamSpecification {
106285
106347
  constructor(props: {
106286
- ResourcePolicy?: GlobalTable_ResourcePolicy;
106348
+ ResourcePolicy: GlobalTable_ResourcePolicy;
106287
106349
  });
106288
106350
  }
106289
106351
 
@@ -112163,6 +112225,8 @@ export declare class Scraper_Destination {
112163
112225
  constructor(props: {
112164
112226
  /** Configuration for Amazon Managed Prometheus metrics destination */
112165
112227
  AmpConfiguration?: Record<string, unknown>;
112228
+ /** Configuration for CloudWatch metrics destination */
112229
+ CloudWatchConfiguration?: Record<string, unknown>;
112166
112230
  });
112167
112231
  }
112168
112232
 
@@ -130350,6 +130414,15 @@ export type Harness_HarnessStatus =
130350
130414
  | "UPDATE_FAILED"
130351
130415
  | "UPDATING";
130352
130416
 
130417
+ export type HarnessEndpoint_HarnessEndpointStatus =
130418
+ | "CREATE_FAILED"
130419
+ | "CREATING"
130420
+ | "DELETE_FAILED"
130421
+ | "DELETING"
130422
+ | "READY"
130423
+ | "UPDATE_FAILED"
130424
+ | "UPDATING";
130425
+
130353
130426
  export type HealthImagingDatastore_DatastoreStatus =
130354
130427
  | "ACTIVE"
130355
130428
  | "CREATE_FAILED"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.33.1",
3
+ "version": "0.34.1",
4
4
  "description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -80,7 +80,7 @@
80
80
  "typescript": "^5.9.3"
81
81
  },
82
82
  "peerDependencies": {
83
- "@intentius/chant": "^0.33.1",
83
+ "@intentius/chant": "^0.34.1",
84
84
  "typescript": "^5.9.3"
85
85
  }
86
86
  }
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const spawnMock = vi.fn();
4
+ vi.mock("@intentius/chant/runtime-adapter", async (importOriginal) => {
5
+ const actual = await importOriginal<typeof import("@intentius/chant/runtime-adapter")>();
6
+ return { ...actual, getRuntime: () => ({ ...actual.getRuntime(), spawn: spawnMock }) };
7
+ });
8
+
9
+ const { observeAwsAmbient, canEnumerate } = await import("./ambient");
10
+
11
+ const ok = (body: unknown) => ({ stdout: JSON.stringify(body), stderr: "", exitCode: 0 });
12
+ const GROUPS = {
13
+ SecurityGroups: [
14
+ { GroupId: "sg-default", GroupName: "default", VpcId: "vpc-1" },
15
+ { GroupId: "sg-mine", GroupName: "web", VpcId: "vpc-1" },
16
+ { GroupId: "sg-stray", GroupName: "leftover", VpcId: "vpc-1" },
17
+ ],
18
+ };
19
+
20
+ // #1278 — a resource nothing points at is invisible to both other observation
21
+ // paths, because both resolve outward from what is declared. "Which of my
22
+ // security groups are unused" is a question about exactly those.
23
+ describe("observeAwsAmbient (#1278)", () => {
24
+ beforeEach(() => spawnMock.mockReset());
25
+
26
+ it("reports groups that exist but are not managed", async () => {
27
+ spawnMock.mockResolvedValue(ok(GROUPS));
28
+ const found = await observeAwsAmbient({
29
+ kinds: ["AWS::EC2::SecurityGroup"],
30
+ observed: { web: { type: "AWS::EC2::SecurityGroup", status: "OK", physicalId: "sg-mine" } },
31
+ });
32
+ // sg-mine is managed, so it is not ambient — it would otherwise be counted twice.
33
+ expect(Object.keys(found).sort()).toEqual(["sg-default", "sg-stray"]);
34
+ expect(found["sg-default"]).toMatchObject({ ambient: true, ownership: "foreign" });
35
+ });
36
+
37
+ it("carries the whole payload, leaving 'unused' to the graph", async () => {
38
+ // Deciding attachment in the reader would put a conclusion in the
39
+ // observation — the mistake liveInternetFacing made and #1271 undid.
40
+ spawnMock.mockResolvedValue(ok(GROUPS));
41
+ const found = await observeAwsAmbient({ kinds: ["AWS::EC2::SecurityGroup"], observed: {} });
42
+ expect(found["sg-default"].attributes).toMatchObject({ GroupName: "default", VpcId: "vpc-1" });
43
+ });
44
+
45
+ it("is bounded by the kinds the project declares", async () => {
46
+ // A project managing security groups is not made to enumerate the account.
47
+ const found = await observeAwsAmbient({ kinds: ["AWS::S3::Bucket"], observed: {} });
48
+ expect(found).toEqual({});
49
+ expect(spawnMock).not.toHaveBeenCalled();
50
+ });
51
+
52
+ it("targets the stack's region", async () => {
53
+ spawnMock.mockResolvedValue(ok(GROUPS));
54
+ await observeAwsAmbient({ kinds: ["AWS::EC2::SecurityGroup"], observed: {}, region: "us-west-2" });
55
+ const argv = spawnMock.mock.calls[0][0] as string[];
56
+ expect(argv).toContain("--region");
57
+ expect(argv).toContain("us-west-2");
58
+ });
59
+
60
+ it("a failed enumeration yields nothing rather than throwing", async () => {
61
+ spawnMock.mockResolvedValue({ stdout: "", stderr: "denied", exitCode: 255 });
62
+ await expect(
63
+ observeAwsAmbient({ kinds: ["AWS::EC2::SecurityGroup"], observed: {} }),
64
+ ).resolves.toEqual({});
65
+ });
66
+
67
+ it("declares which kinds it can enumerate", () => {
68
+ expect(canEnumerate("AWS::EC2::SecurityGroup")).toBe(true);
69
+ expect(canEnumerate("AWS::EC2::Instance")).toBe(false);
70
+ });
71
+ });
package/src/ambient.ts ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Resources that are simply *there* (#1278).
3
+ *
4
+ * `describeResources` answers "what do I manage" and `observeDependencies`
5
+ * answers "what do I rely on". Both resolve outward from the declared estate,
6
+ * so neither can see a resource nothing points at — and "which of my security
7
+ * groups are unused" is precisely a question about resources nothing points at.
8
+ *
9
+ * A state file cannot answer this at all: it knows what it created, and an
10
+ * unused resource is by definition not something it created and attached. A
11
+ * lexicon that can enumerate a kind can.
12
+ *
13
+ * Bounded by the kinds the project declares. A project managing security groups
14
+ * is asked about security groups, not about the account.
15
+ */
16
+
17
+ import { applyAwsEndpointArgv } from "./components/cloud-executor";
18
+ import type { ResourceMetadata } from "@intentius/chant/lexicon";
19
+
20
+ /** Enumerable kinds, and how to list them. Extending this is the way to widen. */
21
+ const ENUMERABLE: Record<string, { argv: string[]; key: string; id: string; name?: string }> = {
22
+ "AWS::EC2::SecurityGroup": {
23
+ argv: ["ec2", "describe-security-groups"],
24
+ key: "SecurityGroups",
25
+ id: "GroupId",
26
+ name: "GroupName",
27
+ },
28
+ // The account's default VPC is the archetype: nothing declares it, nothing
29
+ // in the declared estate points at it, and "which instances are outside a
30
+ // default VPC" cannot be answered without it.
31
+ "AWS::EC2::VPC": {
32
+ argv: ["ec2", "describe-vpcs"],
33
+ key: "Vpcs",
34
+ id: "VpcId",
35
+ },
36
+ // Nobody writes a network interface: EC2 creates one per instance. But the
37
+ // ENI is where a security group is actually attached, so "which of my
38
+ // security groups are unused" is an ENI question and cannot be answered
39
+ // without them. Asked exactly that, an agent queried
40
+ // `kind:EC2::NetworkInterface`, got nothing, and rebuilt the attachment map
41
+ // by hand from twenty-nine raw provider calls.
42
+ "AWS::EC2::NetworkInterface": {
43
+ argv: ["ec2", "describe-network-interfaces"],
44
+ key: "NetworkInterfaces",
45
+ id: "NetworkInterfaceId",
46
+ },
47
+ };
48
+
49
+ /**
50
+ * Kinds a declared kind implies, though nobody writes them.
51
+ *
52
+ * The scan is bounded by what the project declares — a project managing
53
+ * security groups is asked about security groups, not about the account. That
54
+ * bound is right, and it excluded exactly the resource that answers the
55
+ * question: EC2 creates a network interface per instance, and the ENI is where
56
+ * a security group is actually attached. So an estate full of instances had no
57
+ * ENIs recorded, and "which of my security groups are unused" — definitionally
58
+ * an ENI question — could not be answered from the snapshot at all.
59
+ *
60
+ * Declaring an instance is declaring its network interface. This widens the
61
+ * bound by implication rather than abandoning it: still nothing about the
62
+ * account at large.
63
+ */
64
+ const IMPLIED: Record<string, string[]> = {
65
+ "AWS::EC2::Instance": ["AWS::EC2::NetworkInterface"],
66
+ };
67
+
68
+ /** The requested kinds, plus the ones they imply. */
69
+ export function withImplied(kinds: string[]): string[] {
70
+ return [...new Set(kinds.flatMap((k) => [k, ...(IMPLIED[k] ?? [])]))];
71
+ }
72
+
73
+ /** The kinds this reader can enumerate. Declared so a caller can mention
74
+ * `--ambient` without paying for a scan to discover it is relevant. */
75
+ export const AMBIENT_KINDS: string[] = Object.keys(ENUMERABLE);
76
+
77
+ /** True when this lexicon can enumerate the kind, so a caller can say what it skipped. */
78
+ export function canEnumerate(kind: string): boolean {
79
+ return kind in ENUMERABLE;
80
+ }
81
+
82
+ /**
83
+ * List resources of the declared kinds that exist in the account but are
84
+ * neither managed nor already observed.
85
+ *
86
+ * Attachment is deliberately NOT decided here. Whether a security group is
87
+ * "unused" is a graph question — does anything reference it — and the graph
88
+ * answers it once the group is a node. Deciding it in the reader would put a
89
+ * conclusion in the observation, which is the mistake `liveInternetFacing` made
90
+ * and #1271 undid.
91
+ */
92
+ export async function observeAwsAmbient(options: {
93
+ kinds: string[];
94
+ observed: Record<string, ResourceMetadata>;
95
+ region?: string;
96
+ }): Promise<Record<string, ResourceMetadata>> {
97
+ const kinds = withImplied(options.kinds).filter(canEnumerate);
98
+ if (kinds.length === 0) return {};
99
+
100
+ const { getRuntime } = await import("@intentius/chant/runtime-adapter");
101
+ const rt = getRuntime();
102
+ const regionArgs = options.region ? ["--region", options.region] : [];
103
+
104
+ // Physical ids already accounted for, so a managed resource is never also
105
+ // reported as ambient.
106
+ const known = new Set(
107
+ Object.values(options.observed)
108
+ .map((m) => m.physicalId)
109
+ .filter((id): id is string => typeof id === "string"),
110
+ );
111
+
112
+ const ambient: Record<string, ResourceMetadata> = {};
113
+ for (const kind of kinds) {
114
+ const spec = ENUMERABLE[kind];
115
+ try {
116
+ const result = await rt.spawn(
117
+ applyAwsEndpointArgv(["aws", ...spec.argv, ...regionArgs, "--output", "json"], process.env.AWS_ENDPOINT_URL),
118
+ );
119
+ if (result.exitCode !== 0) continue;
120
+ const rows = (JSON.parse(result.stdout)[spec.key] ?? []) as Array<Record<string, unknown>>;
121
+ for (const row of rows) {
122
+ const id = row[spec.id];
123
+ if (typeof id !== "string" || known.has(id)) continue;
124
+ ambient[id] = {
125
+ type: kind,
126
+ status: "OBSERVED",
127
+ physicalId: id,
128
+ // The whole payload, so the graph can decide what "unused" means
129
+ // rather than the reader pre-deciding it.
130
+ attributes: row,
131
+ ownership: "foreign",
132
+ ambient: true,
133
+ };
134
+ }
135
+ } catch {
136
+ // Best-effort per kind: one unreadable kind does not sink the others, and
137
+ // the managed observation is already complete without any of this.
138
+ continue;
139
+ }
140
+ }
141
+ // Same stamps the managed path applies, so one query shape works across both
142
+ // (#1279). Defaults matter most here: a default VPC or security group is
143
+ // ambient by definition — nobody declared it.
144
+ const { stampRegion } = await import("./properties");
145
+ const { stampProviderDefaults } = await import("./defaults");
146
+ return stampProviderDefaults(stampRegion(ambient, options.region));
147
+ }
@@ -246,6 +246,11 @@ export interface AwsDeepObserveOptions {
246
246
  entityNames: string[];
247
247
  entities?: Map<string, { entityType: string; props: Record<string, unknown> }>;
248
248
  stack?: string;
249
+ /** Region this stack is deployed in (#1267). Same reason the thin path takes
250
+ * one (#1261): without it a multi-region estate reads every stack against the
251
+ * ambient region, the out-of-region ones come back empty, and a deep snapshot
252
+ * silently records no properties for them. */
253
+ region?: string;
249
254
  owned?: boolean;
250
255
  }
251
256
 
@@ -270,9 +275,12 @@ export async function observeResourcesDeepAws(
270
275
  const stackName = options.stack ?? options.environment;
271
276
  const endpoint = process.env.AWS_ENDPOINT_URL;
272
277
 
278
+ const regionArgs = options.region ? ["--region", options.region] : [];
279
+
273
280
  const listResult = await rt.spawn(applyAwsEndpointArgv([
274
281
  "aws", "cloudformation", "describe-stack-resources",
275
282
  "--stack-name", stackName,
283
+ ...regionArgs,
276
284
  "--output", "json",
277
285
  ], endpoint));
278
286
 
@@ -334,6 +342,7 @@ export async function observeResourcesDeepAws(
334
342
  "aws", "cloudcontrol", "get-resource",
335
343
  "--type-name", type,
336
344
  "--identifier", identifier,
345
+ ...regionArgs,
337
346
  "--output", "json",
338
347
  ], endpoint));
339
348