@intentius/chant-lexicon-aws 0.33.1 → 0.34.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/ambient.d.ts +39 -0
- package/dist/ambient.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +5 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/defaults.d.ts +44 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/dependencies.d.ts +60 -0
- package/dist/dependencies.d.ts.map +1 -0
- package/dist/generated/index.d.ts +2 -0
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/integrity.json +4 -4
- package/dist/manifest.json +1 -1
- package/dist/meta.json +62 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/properties.d.ts +48 -0
- package/dist/properties.d.ts.map +1 -0
- package/dist/reference-catalog.d.ts.map +1 -1
- package/dist/types/index.d.ts +56 -4
- package/package.json +2 -2
- package/src/ambient.test.ts +71 -0
- package/src/ambient.ts +147 -0
- package/src/deep-observe.ts +9 -0
- package/src/defaults.test.ts +73 -0
- package/src/defaults.ts +97 -0
- package/src/dependencies.test.ts +208 -0
- package/src/dependencies.ts +323 -0
- package/src/generated/index.d.ts +56 -4
- package/src/generated/index.ts +2 -0
- package/src/generated/lexicon-aws.json +62 -1
- package/src/plugin.ts +65 -64
- package/src/properties.test.ts +113 -0
- package/src/properties.ts +166 -0
- package/src/reference-catalog.ts +24 -3
package/dist/integrity.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
5
|
-
"meta.json": "
|
|
6
|
-
"types/index.d.ts": "
|
|
4
|
+
"manifest.json": "3c7b5e21a39f86218b481199f51f2d045af99311e67dbae6d3f4f0a7a3569506",
|
|
5
|
+
"meta.json": "ffe08a1fc279b969d8760332917ddaa0fdf2eca083f861d608f2c55ee8ec147d",
|
|
6
|
+
"types/index.d.ts": "cd6ca943e0753ae32530aadad245b72f910990d5ee7244d5e79cf22f9ff0cf1f",
|
|
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": "
|
|
62
|
+
"composite": "b23b837556489ce43cf6d8ba4ebb13ecf700868ac0cd8698be4af926585915c3"
|
|
63
63
|
}
|
package/dist/manifest.json
CHANGED
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
|
|
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
|
},
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,
|
|
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,
|
|
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"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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. */
|
|
@@ -57089,7 +57121,7 @@ export declare class ContainerRecipe_EbsInstanceBlockDeviceSpecification {
|
|
|
57089
57121
|
/** Use to override the device's volume size. */
|
|
57090
57122
|
VolumeSize?: number;
|
|
57091
57123
|
/** Use to override the device's volume type. */
|
|
57092
|
-
VolumeType?: "gp2" | "gp3" | "io1" | "io2" | "sc1" | "st1" | "standard";
|
|
57124
|
+
VolumeType?: "gp2" | "gp3" | "gp3a" | "io1" | "io2" | "io2a" | "sc1" | "st1" | "standard";
|
|
57093
57125
|
});
|
|
57094
57126
|
}
|
|
57095
57127
|
|
|
@@ -75141,7 +75173,7 @@ export declare class GlobalTable_ReplicaSSESpecification {
|
|
|
75141
75173
|
|
|
75142
75174
|
export declare class GlobalTable_ReplicaStreamSpecification {
|
|
75143
75175
|
constructor(props: {
|
|
75144
|
-
ResourcePolicy
|
|
75176
|
+
ResourcePolicy: GlobalTable_ResourcePolicy;
|
|
75145
75177
|
});
|
|
75146
75178
|
}
|
|
75147
75179
|
|
|
@@ -76693,6 +76725,13 @@ export declare class HarnessBedrockModelConfig {
|
|
|
76693
76725
|
});
|
|
76694
76726
|
}
|
|
76695
76727
|
|
|
76728
|
+
export declare class HarnessEndpoint_Tag {
|
|
76729
|
+
constructor(props: {
|
|
76730
|
+
Key: string;
|
|
76731
|
+
Value: string;
|
|
76732
|
+
});
|
|
76733
|
+
}
|
|
76734
|
+
|
|
76696
76735
|
export declare class HarnessEnvironmentArtifact {
|
|
76697
76736
|
constructor(props: {
|
|
76698
76737
|
ContainerConfiguration?: Harness_ContainerConfiguration;
|
|
@@ -78724,7 +78763,7 @@ export declare class ImageRecipe_EbsInstanceBlockDeviceSpecification {
|
|
|
78724
78763
|
/** Use to override the device's volume size. */
|
|
78725
78764
|
VolumeSize?: number;
|
|
78726
78765
|
/** Use to override the device's volume type. */
|
|
78727
|
-
VolumeType?: "gp2" | "gp3" | "io1" | "io2" | "sc1" | "st1" | "standard";
|
|
78766
|
+
VolumeType?: "gp2" | "gp3" | "gp3a" | "io1" | "io2" | "io2a" | "sc1" | "st1" | "standard";
|
|
78728
78767
|
});
|
|
78729
78768
|
}
|
|
78730
78769
|
|
|
@@ -86435,7 +86474,9 @@ export declare class Listener_RedirectConfig {
|
|
|
86435
86474
|
|
|
86436
86475
|
export declare class Listener_Tag {
|
|
86437
86476
|
constructor(props: {
|
|
86477
|
+
/** The key of the tag. */
|
|
86438
86478
|
Key: string;
|
|
86479
|
+
/** The value of the tag. */
|
|
86439
86480
|
Value: string;
|
|
86440
86481
|
});
|
|
86441
86482
|
}
|
|
@@ -106283,7 +106324,7 @@ export declare class ReplicaSSESpecification {
|
|
|
106283
106324
|
|
|
106284
106325
|
export declare class ReplicaStreamSpecification {
|
|
106285
106326
|
constructor(props: {
|
|
106286
|
-
ResourcePolicy
|
|
106327
|
+
ResourcePolicy: GlobalTable_ResourcePolicy;
|
|
106287
106328
|
});
|
|
106288
106329
|
}
|
|
106289
106330
|
|
|
@@ -112163,6 +112204,8 @@ export declare class Scraper_Destination {
|
|
|
112163
112204
|
constructor(props: {
|
|
112164
112205
|
/** Configuration for Amazon Managed Prometheus metrics destination */
|
|
112165
112206
|
AmpConfiguration?: Record<string, unknown>;
|
|
112207
|
+
/** Configuration for CloudWatch metrics destination */
|
|
112208
|
+
CloudWatchConfiguration?: Record<string, unknown>;
|
|
112166
112209
|
});
|
|
112167
112210
|
}
|
|
112168
112211
|
|
|
@@ -130350,6 +130393,15 @@ export type Harness_HarnessStatus =
|
|
|
130350
130393
|
| "UPDATE_FAILED"
|
|
130351
130394
|
| "UPDATING";
|
|
130352
130395
|
|
|
130396
|
+
export type HarnessEndpoint_HarnessEndpointStatus =
|
|
130397
|
+
| "CREATE_FAILED"
|
|
130398
|
+
| "CREATING"
|
|
130399
|
+
| "DELETE_FAILED"
|
|
130400
|
+
| "DELETING"
|
|
130401
|
+
| "READY"
|
|
130402
|
+
| "UPDATE_FAILED"
|
|
130403
|
+
| "UPDATING";
|
|
130404
|
+
|
|
130353
130405
|
export type HealthImagingDatastore_DatastoreStatus =
|
|
130354
130406
|
| "ACTIVE"
|
|
130355
130407
|
| "CREATE_FAILED"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-aws",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
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.
|
|
83
|
+
"@intentius/chant": "^0.34.0",
|
|
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
|
+
}
|
package/src/deep-observe.ts
CHANGED
|
@@ -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
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { stampProviderDefaults, canDetectDefault, DEFAULT_AWARE_KINDS } from "./defaults";
|
|
3
|
+
import type { ResourceMetadata } from "@intentius/chant/lexicon";
|
|
4
|
+
|
|
5
|
+
const res = (type: string, attributes: Record<string, unknown>): ResourceMetadata => ({
|
|
6
|
+
type,
|
|
7
|
+
status: "OBSERVED",
|
|
8
|
+
physicalId: "x",
|
|
9
|
+
attributes,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const flag = (type: string, attributes: Record<string, unknown>) =>
|
|
13
|
+
stampProviderDefaults({ r: res(type, attributes) }).r.attributes?.providerDefault;
|
|
14
|
+
|
|
15
|
+
// Every account arrives with resources nobody wrote. Asked which security
|
|
16
|
+
// groups were unused, agents split three ways on the same correct set of four,
|
|
17
|
+
// because chant gave them no way to tell a VPC's default group from one someone
|
|
18
|
+
// created.
|
|
19
|
+
describe("provider defaults (#1278)", () => {
|
|
20
|
+
it("reads the marker AWS already sets, per kind", () => {
|
|
21
|
+
expect(flag("AWS::EC2::VPC", { IsDefault: true })).toBe(true);
|
|
22
|
+
expect(flag("AWS::EC2::Subnet", { DefaultForAz: true })).toBe(true);
|
|
23
|
+
expect(flag("AWS::EC2::NetworkAcl", { IsDefault: true })).toBe(true);
|
|
24
|
+
expect(flag("AWS::KMS::Key", { KeyManager: "AWS" })).toBe(true);
|
|
25
|
+
expect(flag("AWS::IAM::ManagedPolicy", { Arn: "arn:aws:iam::aws:policy/ReadOnlyAccess" })).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("does not mark what the account holder created", () => {
|
|
29
|
+
expect(flag("AWS::EC2::VPC", { IsDefault: false })).toBeUndefined();
|
|
30
|
+
expect(flag("AWS::EC2::Subnet", { DefaultForAz: false })).toBeUndefined();
|
|
31
|
+
expect(flag("AWS::KMS::Key", { KeyManager: "CUSTOMER" })).toBeUndefined();
|
|
32
|
+
expect(flag("AWS::IAM::ManagedPolicy", { Arn: "arn:aws:iam::000000000000:policy/Mine" })).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("treats the reserved group name as the marker it is", () => {
|
|
36
|
+
// AWS refuses `default` on create, so a group carrying it is the one AWS
|
|
37
|
+
// made with the VPC.
|
|
38
|
+
expect(flag("AWS::EC2::SecurityGroup", { GroupName: "default" })).toBe(true);
|
|
39
|
+
expect(flag("AWS::EC2::SecurityGroup", { GroupName: "default-web" })).toBeUndefined();
|
|
40
|
+
expect(flag("AWS::EC2::SecurityGroup", { GroupName: "webSecurityGroup" })).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("finds the main route table through its associations", () => {
|
|
44
|
+
expect(flag("AWS::EC2::RouteTable", { Associations: [{ Main: true }] })).toBe(true);
|
|
45
|
+
expect(flag("AWS::EC2::RouteTable", { Associations: [{ Main: false }, { Main: true }] })).toBe(true);
|
|
46
|
+
expect(flag("AWS::EC2::RouteTable", { Associations: [{ Main: false }] })).toBeUndefined();
|
|
47
|
+
expect(flag("AWS::EC2::RouteTable", {})).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("leaves a kind it cannot judge unmarked rather than guessing", () => {
|
|
51
|
+
// Absent means "not a default, or chant cannot tell". A blanket false would
|
|
52
|
+
// hide the difference.
|
|
53
|
+
expect(flag("AWS::EC2::Instance", { InstanceId: "i-1" })).toBeUndefined();
|
|
54
|
+
expect(canDetectDefault("AWS::EC2::Instance")).toBe(false);
|
|
55
|
+
expect(canDetectDefault("AWS::EC2::VPC")).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("keeps every attribute it was given", () => {
|
|
59
|
+
const out = stampProviderDefaults({
|
|
60
|
+
r: res("AWS::EC2::SecurityGroup", { GroupName: "default", GroupId: "sg-1", region: "us-east-1" }),
|
|
61
|
+
}).r.attributes;
|
|
62
|
+
expect(out).toMatchObject({ GroupId: "sg-1", region: "us-east-1", providerDefault: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("survives a payload it cannot read", () => {
|
|
66
|
+
expect(flag("AWS::EC2::RouteTable", { Associations: "not-a-list" })).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("reports the kinds it can judge, so a caller need not guess", () => {
|
|
70
|
+
expect(DEFAULT_AWARE_KINDS).toContain("AWS::EC2::SecurityGroup");
|
|
71
|
+
expect(DEFAULT_AWARE_KINDS.every(canDetectDefault)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|