@intentius/chant-lexicon-aws 0.124.45 → 0.124.46
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/capabilities/mapping.json +150 -0
- package/dist/capabilities/pack.d.ts +8 -0
- package/dist/capabilities/pack.d.ts.map +1 -0
- package/dist/capabilities/pack.js +2 -0
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +2 -0
- package/package.json +2 -2
- package/src/capabilities/mapping.json +150 -0
- package/src/capabilities/pack.ts +9 -0
- package/src/capabilities.test.ts +182 -0
- package/src/graph-sub-edges.test.ts +101 -0
- package/src/plugin.ts +2 -0
- package/src/testdata/capabilities-ledger/estate.ts +161 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lexicon": "aws",
|
|
3
|
+
"types": "kind",
|
|
4
|
+
"refs": "cloudformation",
|
|
5
|
+
"labels": {
|
|
6
|
+
"AWS::EC2::VPC": "Vpc",
|
|
7
|
+
"AWS::EC2::NetworkInterface": "NetworkInterface",
|
|
8
|
+
"AWS::EC2::Volume": "Volume",
|
|
9
|
+
"AWS::Lambda::Function": "Function",
|
|
10
|
+
"AWS::Lambda::Url": "FunctionUrl",
|
|
11
|
+
"AWS::IAM::InstanceProfile": "InstanceProfile",
|
|
12
|
+
"AWS::IAM::Role": "Role",
|
|
13
|
+
"AWS::IAM::Policy": "Policy",
|
|
14
|
+
"AWS::IAM::ManagedPolicy": "Policy",
|
|
15
|
+
"AWS::S3::BucketPolicy": "ResourcePolicy",
|
|
16
|
+
"AWS::SQS::QueuePolicy": "ResourcePolicy",
|
|
17
|
+
"AWS::S3::Bucket": "Bucket",
|
|
18
|
+
"AWS::DynamoDB::Table": "Table",
|
|
19
|
+
"AWS::DynamoDB::GlobalTable": "Table",
|
|
20
|
+
"AWS::RDS::DBInstance": "Database",
|
|
21
|
+
"AWS::RDS::DBCluster": "Database",
|
|
22
|
+
"AWS::SecretsManager::Secret": "Secret",
|
|
23
|
+
"AWS::SQS::Queue": "Queue",
|
|
24
|
+
"AWS::EFS::FileSystem": "FileSystem",
|
|
25
|
+
"AWS::SSM::Parameter": "Parameter",
|
|
26
|
+
"AWS::ECS::Service": "Service",
|
|
27
|
+
"AWS::ECS::TaskDefinition": "TaskDefinition",
|
|
28
|
+
"AWS::RDS::DBSubnetGroup": "SubnetGroup",
|
|
29
|
+
"AWS::EC2::NatGateway": "NatGateway",
|
|
30
|
+
"AWS::ElasticLoadBalancingV2::LoadBalancer": "LoadBalancer"
|
|
31
|
+
},
|
|
32
|
+
"edges": {
|
|
33
|
+
"Instance": {
|
|
34
|
+
"IamInstanceProfile": "profile",
|
|
35
|
+
"VpcId": "vpc"
|
|
36
|
+
},
|
|
37
|
+
"LaunchTemplate": {
|
|
38
|
+
"LaunchTemplateData": {
|
|
39
|
+
"InstanceProfile": "profile"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"Subnet": {
|
|
43
|
+
"VpcId": "vpc"
|
|
44
|
+
},
|
|
45
|
+
"SecurityGroup": {
|
|
46
|
+
"VpcId": "vpc"
|
|
47
|
+
},
|
|
48
|
+
"NetworkInterface": {
|
|
49
|
+
"SubnetId": "subnet",
|
|
50
|
+
"GroupSet": "sg"
|
|
51
|
+
},
|
|
52
|
+
"Database": {
|
|
53
|
+
"VPCSecurityGroups": "sg",
|
|
54
|
+
"VpcSecurityGroupIds": "sg",
|
|
55
|
+
"DBSubnetGroupName": "subnetGroup"
|
|
56
|
+
},
|
|
57
|
+
"Function": {
|
|
58
|
+
"Role": "role",
|
|
59
|
+
"VpcConfig": {
|
|
60
|
+
"SecurityGroup": "sg",
|
|
61
|
+
"Subnet": "subnet"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"FunctionUrl": {
|
|
65
|
+
"TargetFunctionArn": "function"
|
|
66
|
+
},
|
|
67
|
+
"InstanceProfile": {
|
|
68
|
+
"Roles": "role"
|
|
69
|
+
},
|
|
70
|
+
"Policy": {
|
|
71
|
+
"Roles": "attachedTo"
|
|
72
|
+
},
|
|
73
|
+
"Role": {
|
|
74
|
+
"ManagedPolicyArns": "-attachedTo"
|
|
75
|
+
},
|
|
76
|
+
"ResourcePolicy": {
|
|
77
|
+
"Bucket": "policyOf",
|
|
78
|
+
"Queues": "policyOf"
|
|
79
|
+
},
|
|
80
|
+
"Service": {
|
|
81
|
+
"TaskDefinition": "task",
|
|
82
|
+
"NetworkConfiguration": {
|
|
83
|
+
"SecurityGroup": "sg",
|
|
84
|
+
"Subnet": "subnet"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"TaskDefinition": {
|
|
88
|
+
"TaskRoleArn": "role"
|
|
89
|
+
},
|
|
90
|
+
"SubnetGroup": {
|
|
91
|
+
"SubnetIds": "subnet"
|
|
92
|
+
},
|
|
93
|
+
"NatGateway": {
|
|
94
|
+
"SubnetId": "subnet"
|
|
95
|
+
},
|
|
96
|
+
"LoadBalancer": {
|
|
97
|
+
"Subnets": "subnet",
|
|
98
|
+
"SecurityGroups": "sg"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"attrs": {
|
|
102
|
+
"FunctionUrl": {
|
|
103
|
+
"authType": "AuthType"
|
|
104
|
+
},
|
|
105
|
+
"Role": {
|
|
106
|
+
"name": "RoleName"
|
|
107
|
+
},
|
|
108
|
+
"Bucket": {
|
|
109
|
+
"name": "BucketName",
|
|
110
|
+
"acl": "AccessControl",
|
|
111
|
+
"blockPublicPolicy": "PublicAccessBlockConfiguration.BlockPublicPolicy"
|
|
112
|
+
},
|
|
113
|
+
"Table": {
|
|
114
|
+
"name": "TableName"
|
|
115
|
+
},
|
|
116
|
+
"Database": {
|
|
117
|
+
"encrypted": "StorageEncrypted"
|
|
118
|
+
},
|
|
119
|
+
"FileSystem": {
|
|
120
|
+
"encrypted": "Encrypted"
|
|
121
|
+
},
|
|
122
|
+
"Volume": {
|
|
123
|
+
"encrypted": "Encrypted"
|
|
124
|
+
},
|
|
125
|
+
"Parameter": {
|
|
126
|
+
"paramType": "Type"
|
|
127
|
+
},
|
|
128
|
+
"Vpc": {
|
|
129
|
+
"isDefault": "IsDefault"
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"documents": {
|
|
133
|
+
"Role": {
|
|
134
|
+
"AssumeRolePolicyDocument": "trust",
|
|
135
|
+
"Policies[].PolicyDocument": "permission"
|
|
136
|
+
},
|
|
137
|
+
"Policy": {
|
|
138
|
+
"PolicyDocument": "permission"
|
|
139
|
+
},
|
|
140
|
+
"ResourcePolicy": {
|
|
141
|
+
"PolicyDocument": "resource"
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"tags": [
|
|
145
|
+
"Tags"
|
|
146
|
+
],
|
|
147
|
+
"classificationTags": [
|
|
148
|
+
"data-classification"
|
|
149
|
+
]
|
|
150
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The aws lexicon's capability pack (#4375): its mapping of CloudFormation
|
|
3
|
+
* kinds onto the lexicon-neutral view the capability rules read. All data;
|
|
4
|
+
* CloudFormation policy documents are JSON already, so no adapter code.
|
|
5
|
+
*/
|
|
6
|
+
import type { CapabilityPack } from "@intentius/chant/capabilities/view";
|
|
7
|
+
export declare const awsCapabilityPack: CapabilityPack;
|
|
8
|
+
//# sourceMappingURL=pack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pack.d.ts","sourceRoot":"","sources":["../../src/capabilities/pack.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAqB,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAG5F,eAAO,MAAM,iBAAiB,EAAE,cAA0D,CAAC"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "8c88e8750ff98569d97921403ae44dfba0065e512d6dee2de6a912897cd84e66",
|
|
5
5
|
"meta.json": "6f48557ed4203cbdeafdb47e7270fdf3533f6d6e6189987246301ed8e72170e9",
|
|
6
6
|
"types/index.d.ts": "8651f9ae166668f4311eea1fe61c3fe235f345947951c1d23d1df24a4fd3384f",
|
|
7
7
|
"rules/hardcoded-region.ts": "957eadcaa9f39cc17d21437565d7a94607d9b082cf28918b178fa067f07498a9",
|
|
@@ -75,5 +75,5 @@
|
|
|
75
75
|
"skills/chant-aws-eks.md": "8789255709ff004ad0a875fd5999edcdc66fc6e33d710db058d9f42703bcfdfe",
|
|
76
76
|
"skills/chant-aws-carve-terraform.md": "8780b94ce60aded935938d5ef20ea667738354051da1bf9ca6d70d9886f65d21"
|
|
77
77
|
},
|
|
78
|
-
"composite": "
|
|
78
|
+
"composite": "c3e022363a232d4ee358a6f31700ecb1ee3d51b871d2a1efddb4655291562233"
|
|
79
79
|
}
|
package/dist/manifest.json
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAqQ,MAAM,0BAA0B,CAAC;AAwCjU;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAiBnD,eAAO,MAAM,SAAS,EAAE,aAgkCvB,CAAC"}
|
package/dist/plugin.js
CHANGED
|
@@ -2,6 +2,7 @@ import { compositeCatalog } from "./composites/catalog.js";
|
|
|
2
2
|
import { createRequire } from "module";
|
|
3
3
|
import { awsBehaviourKinds } from "./behaviour-kinds.js";
|
|
4
4
|
import { detectTemplate } from "./detect.js";
|
|
5
|
+
import { awsCapabilityPack } from "./capabilities/pack.js";
|
|
5
6
|
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
6
7
|
import { discoverLintRules } from "@intentius/chant/lint/discover";
|
|
7
8
|
import { postSynthChecks as postSynthCheckList } from "./lint/post-synth/index.js";
|
|
@@ -63,6 +64,7 @@ export const awsPlugin = {
|
|
|
63
64
|
auditCatalog: () => awsAuditCatalog,
|
|
64
65
|
// Live edge reconstruction for `chant graph --live` (#778).
|
|
65
66
|
referenceCatalog: awsReferenceCatalog,
|
|
67
|
+
capabilityPack: awsCapabilityPack,
|
|
66
68
|
lintRules() {
|
|
67
69
|
const rulesDir = join(dirname(fileURLToPath(import.meta.url)), "lint", "rules");
|
|
68
70
|
return discoverLintRules(rulesDir, import.meta.url);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-aws",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.46",
|
|
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,6 +80,6 @@
|
|
|
80
80
|
"typescript": "^5.9.3"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
|
-
"@intentius/chant": "^0.124.
|
|
83
|
+
"@intentius/chant": "^0.124.46"
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lexicon": "aws",
|
|
3
|
+
"types": "kind",
|
|
4
|
+
"refs": "cloudformation",
|
|
5
|
+
"labels": {
|
|
6
|
+
"AWS::EC2::VPC": "Vpc",
|
|
7
|
+
"AWS::EC2::NetworkInterface": "NetworkInterface",
|
|
8
|
+
"AWS::EC2::Volume": "Volume",
|
|
9
|
+
"AWS::Lambda::Function": "Function",
|
|
10
|
+
"AWS::Lambda::Url": "FunctionUrl",
|
|
11
|
+
"AWS::IAM::InstanceProfile": "InstanceProfile",
|
|
12
|
+
"AWS::IAM::Role": "Role",
|
|
13
|
+
"AWS::IAM::Policy": "Policy",
|
|
14
|
+
"AWS::IAM::ManagedPolicy": "Policy",
|
|
15
|
+
"AWS::S3::BucketPolicy": "ResourcePolicy",
|
|
16
|
+
"AWS::SQS::QueuePolicy": "ResourcePolicy",
|
|
17
|
+
"AWS::S3::Bucket": "Bucket",
|
|
18
|
+
"AWS::DynamoDB::Table": "Table",
|
|
19
|
+
"AWS::DynamoDB::GlobalTable": "Table",
|
|
20
|
+
"AWS::RDS::DBInstance": "Database",
|
|
21
|
+
"AWS::RDS::DBCluster": "Database",
|
|
22
|
+
"AWS::SecretsManager::Secret": "Secret",
|
|
23
|
+
"AWS::SQS::Queue": "Queue",
|
|
24
|
+
"AWS::EFS::FileSystem": "FileSystem",
|
|
25
|
+
"AWS::SSM::Parameter": "Parameter",
|
|
26
|
+
"AWS::ECS::Service": "Service",
|
|
27
|
+
"AWS::ECS::TaskDefinition": "TaskDefinition",
|
|
28
|
+
"AWS::RDS::DBSubnetGroup": "SubnetGroup",
|
|
29
|
+
"AWS::EC2::NatGateway": "NatGateway",
|
|
30
|
+
"AWS::ElasticLoadBalancingV2::LoadBalancer": "LoadBalancer"
|
|
31
|
+
},
|
|
32
|
+
"edges": {
|
|
33
|
+
"Instance": {
|
|
34
|
+
"IamInstanceProfile": "profile",
|
|
35
|
+
"VpcId": "vpc"
|
|
36
|
+
},
|
|
37
|
+
"LaunchTemplate": {
|
|
38
|
+
"LaunchTemplateData": {
|
|
39
|
+
"InstanceProfile": "profile"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"Subnet": {
|
|
43
|
+
"VpcId": "vpc"
|
|
44
|
+
},
|
|
45
|
+
"SecurityGroup": {
|
|
46
|
+
"VpcId": "vpc"
|
|
47
|
+
},
|
|
48
|
+
"NetworkInterface": {
|
|
49
|
+
"SubnetId": "subnet",
|
|
50
|
+
"GroupSet": "sg"
|
|
51
|
+
},
|
|
52
|
+
"Database": {
|
|
53
|
+
"VPCSecurityGroups": "sg",
|
|
54
|
+
"VpcSecurityGroupIds": "sg",
|
|
55
|
+
"DBSubnetGroupName": "subnetGroup"
|
|
56
|
+
},
|
|
57
|
+
"Function": {
|
|
58
|
+
"Role": "role",
|
|
59
|
+
"VpcConfig": {
|
|
60
|
+
"SecurityGroup": "sg",
|
|
61
|
+
"Subnet": "subnet"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"FunctionUrl": {
|
|
65
|
+
"TargetFunctionArn": "function"
|
|
66
|
+
},
|
|
67
|
+
"InstanceProfile": {
|
|
68
|
+
"Roles": "role"
|
|
69
|
+
},
|
|
70
|
+
"Policy": {
|
|
71
|
+
"Roles": "attachedTo"
|
|
72
|
+
},
|
|
73
|
+
"Role": {
|
|
74
|
+
"ManagedPolicyArns": "-attachedTo"
|
|
75
|
+
},
|
|
76
|
+
"ResourcePolicy": {
|
|
77
|
+
"Bucket": "policyOf",
|
|
78
|
+
"Queues": "policyOf"
|
|
79
|
+
},
|
|
80
|
+
"Service": {
|
|
81
|
+
"TaskDefinition": "task",
|
|
82
|
+
"NetworkConfiguration": {
|
|
83
|
+
"SecurityGroup": "sg",
|
|
84
|
+
"Subnet": "subnet"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"TaskDefinition": {
|
|
88
|
+
"TaskRoleArn": "role"
|
|
89
|
+
},
|
|
90
|
+
"SubnetGroup": {
|
|
91
|
+
"SubnetIds": "subnet"
|
|
92
|
+
},
|
|
93
|
+
"NatGateway": {
|
|
94
|
+
"SubnetId": "subnet"
|
|
95
|
+
},
|
|
96
|
+
"LoadBalancer": {
|
|
97
|
+
"Subnets": "subnet",
|
|
98
|
+
"SecurityGroups": "sg"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"attrs": {
|
|
102
|
+
"FunctionUrl": {
|
|
103
|
+
"authType": "AuthType"
|
|
104
|
+
},
|
|
105
|
+
"Role": {
|
|
106
|
+
"name": "RoleName"
|
|
107
|
+
},
|
|
108
|
+
"Bucket": {
|
|
109
|
+
"name": "BucketName",
|
|
110
|
+
"acl": "AccessControl",
|
|
111
|
+
"blockPublicPolicy": "PublicAccessBlockConfiguration.BlockPublicPolicy"
|
|
112
|
+
},
|
|
113
|
+
"Table": {
|
|
114
|
+
"name": "TableName"
|
|
115
|
+
},
|
|
116
|
+
"Database": {
|
|
117
|
+
"encrypted": "StorageEncrypted"
|
|
118
|
+
},
|
|
119
|
+
"FileSystem": {
|
|
120
|
+
"encrypted": "Encrypted"
|
|
121
|
+
},
|
|
122
|
+
"Volume": {
|
|
123
|
+
"encrypted": "Encrypted"
|
|
124
|
+
},
|
|
125
|
+
"Parameter": {
|
|
126
|
+
"paramType": "Type"
|
|
127
|
+
},
|
|
128
|
+
"Vpc": {
|
|
129
|
+
"isDefault": "IsDefault"
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"documents": {
|
|
133
|
+
"Role": {
|
|
134
|
+
"AssumeRolePolicyDocument": "trust",
|
|
135
|
+
"Policies[].PolicyDocument": "permission"
|
|
136
|
+
},
|
|
137
|
+
"Policy": {
|
|
138
|
+
"PolicyDocument": "permission"
|
|
139
|
+
},
|
|
140
|
+
"ResourcePolicy": {
|
|
141
|
+
"PolicyDocument": "resource"
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"tags": [
|
|
145
|
+
"Tags"
|
|
146
|
+
],
|
|
147
|
+
"classificationTags": [
|
|
148
|
+
"data-classification"
|
|
149
|
+
]
|
|
150
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The aws lexicon's capability pack (#4375): its mapping of CloudFormation
|
|
3
|
+
* kinds onto the lexicon-neutral view the capability rules read. All data;
|
|
4
|
+
* CloudFormation policy documents are JSON already, so no adapter code.
|
|
5
|
+
*/
|
|
6
|
+
import type { CapabilityMapping, CapabilityPack } from "@intentius/chant/capabilities/view";
|
|
7
|
+
import mapping from "./mapping.json" with { type: "json" };
|
|
8
|
+
|
|
9
|
+
export const awsCapabilityPack: CapabilityPack = { mapping: mapping as CapabilityMapping };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability catalog over CloudFormation kinds (#4375).
|
|
3
|
+
*
|
|
4
|
+
* `testdata/capabilities-ledger/estate.ts` is the CloudFormation form of the
|
|
5
|
+
* estate chant#4361 measured the rule language on. Its hand-checked answers
|
|
6
|
+
* are identity reach 13, exfiltration 3 and blast radius 15, the same as the
|
|
7
|
+
* Terraform form's (lexicons/terraform/src/capabilities.test.ts).
|
|
8
|
+
*
|
|
9
|
+
* Three of those pairs are proved only by a reference inside a `Sub`
|
|
10
|
+
* (``Sub`${assets.Arn}/*` ``), which the graph carries as `{ $sub }` since
|
|
11
|
+
* chant#4373.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { describe, expect, it } from "vitest";
|
|
16
|
+
import { discover } from "@intentius/chant/discovery/index";
|
|
17
|
+
import { buildGraphIr, type GraphIR } from "@intentius/chant/graph-ir";
|
|
18
|
+
import { enrichEffectiveTopology } from "@intentius/chant/graph-effective";
|
|
19
|
+
import { CapabilityEngine } from "@intentius/chant/capabilities/matcher";
|
|
20
|
+
import { buildView, displayName } from "@intentius/chant/capabilities/view";
|
|
21
|
+
import { lexiconSupport, resolvePacks, shippedRules, isPublic } from "@intentius/chant/capabilities/catalog";
|
|
22
|
+
import { awsCapabilityPack } from "./capabilities/pack";
|
|
23
|
+
|
|
24
|
+
const LEDGER = join(import.meta.dirname, "testdata", "capabilities-ledger");
|
|
25
|
+
|
|
26
|
+
const engineFor = (g: GraphIR) => new CapabilityEngine(buildView(g, resolvePacks([awsCapabilityPack])), shippedRules().rules);
|
|
27
|
+
|
|
28
|
+
const ir: GraphIR = buildGraphIr((await discover(LEDGER)).entities, LEDGER);
|
|
29
|
+
const engine = engineFor(ir);
|
|
30
|
+
|
|
31
|
+
const set = (e: CapabilityEngine, name: string, args: unknown[] = []) =>
|
|
32
|
+
[...e.set(name, e.bind(name, args as never)).keys()].map(displayName).sort();
|
|
33
|
+
const pairs = (e: CapabilityEngine, name: string, args: unknown[] = []) =>
|
|
34
|
+
[...e.relation(name, e.bind(name, args as never)).out].flatMap(([a, m]) => [...m.keys()].map((b) => `${displayName(a)} ${displayName(b)}`)).sort();
|
|
35
|
+
|
|
36
|
+
const REACH = [
|
|
37
|
+
"auditRole ordersTable",
|
|
38
|
+
"batchRole auditRole",
|
|
39
|
+
"batchRole ordersTable",
|
|
40
|
+
"deployRole customerData",
|
|
41
|
+
"deployRole dbSecret",
|
|
42
|
+
"reporterRole customerData",
|
|
43
|
+
"webRole assets",
|
|
44
|
+
"webRole customerData",
|
|
45
|
+
"webRole dbSecret",
|
|
46
|
+
"webRole deployRole",
|
|
47
|
+
"workerRole assets",
|
|
48
|
+
"workerRole dbSecret",
|
|
49
|
+
"workerRole ordersTable",
|
|
50
|
+
];
|
|
51
|
+
const EXFIL = ["reporter customerData", "webServer customerData", "webServer dbSecret"];
|
|
52
|
+
const BLAST_STARTS = ["webServer", "reporter", "batch"];
|
|
53
|
+
const BLAST = [
|
|
54
|
+
...["webRole", "assets", "deployRole", "customerData", "dbSecret", "worker", "adminBox", "workerRole", "ordersTable", "customersDb"].map((x) => `webServer ${x}`),
|
|
55
|
+
"reporter reporterRole",
|
|
56
|
+
"reporter customerData",
|
|
57
|
+
"batch batchRole",
|
|
58
|
+
"batch auditRole",
|
|
59
|
+
"batch ordersTable",
|
|
60
|
+
].sort();
|
|
61
|
+
/** The pairs only a `Sub` reference proves (chant#4373). */
|
|
62
|
+
const NEEDS_SUB = {
|
|
63
|
+
reach: ["reporterRole customerData", "webRole assets", "workerRole assets"],
|
|
64
|
+
exfil: ["reporter customerData"],
|
|
65
|
+
blast: ["reporter customerData", "webServer assets"],
|
|
66
|
+
};
|
|
67
|
+
const blastOf = (e: CapabilityEngine) => pairs(e, "blast").filter((p) => BLAST_STARTS.includes(p.split(" ")[0]));
|
|
68
|
+
|
|
69
|
+
describe("the ledger's hand-checked answers on the CloudFormation form", () => {
|
|
70
|
+
it("identity reach 13, exfiltration 3, blast radius 15, three of them proved only by a Sub reference (chant#4373)", () => {
|
|
71
|
+
expect(pairs(engine, "reaches")).toEqual(REACH);
|
|
72
|
+
expect(pairs(engine, "exfiltrates")).toEqual(EXFIL);
|
|
73
|
+
expect(blastOf(engine)).toEqual(BLAST);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("leaves no statement's resource unresolved", () => {
|
|
77
|
+
const unresolved = [...engine.view.nodes.values()].filter((n) => n.label === "Statement" && n.attrs.unresolved);
|
|
78
|
+
expect(unresolved.map((n) => n.id)).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("finds the Sub-only pairs through the Sub's own reference", () => {
|
|
82
|
+
for (const p of NEEDS_SUB.reach) expect(pairs(engine, "reaches")).toContain(p);
|
|
83
|
+
const ev = engine.relation("reaches").out.get("reporterRole")!.get("customerData")!;
|
|
84
|
+
expect(ev.hops.at(-1)).toMatchObject({ to: "customerData", toAttr: "Arn", edge: "resource" });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("each capability on CloudFormation kinds", () => {
|
|
89
|
+
it("internet-facing, open-ingress and internet-reachable, the open group reached only through a launch template", () => {
|
|
90
|
+
expect(set(engine, "internet-facing")).toEqual(["adminBox", "webServer"]);
|
|
91
|
+
expect(set(engine, "open-ingress", [22])).toEqual(["webServer"]);
|
|
92
|
+
expect(set(engine, "internet-reachable")).toEqual(["reporter", "webServer"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("network reads the standalone SecurityGroupIngress resource as well as inline rules", () => {
|
|
96
|
+
expect(pairs(engine, "network")).toEqual([
|
|
97
|
+
"adminBox customersDb",
|
|
98
|
+
"webServer adminBox",
|
|
99
|
+
"webServer worker",
|
|
100
|
+
"worker customersDb",
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("stores-data, holds-secret (by data-classification tag) and encrypted-at-rest", () => {
|
|
105
|
+
expect(set(engine, "stores-data")).toEqual(["assets", "customerData", "customersDb", "dbSecret", "ordersTable"]);
|
|
106
|
+
expect(set(engine, "holds-secret")).toEqual(["customerData", "customersDb", "dbSecret"]);
|
|
107
|
+
expect(set(engine, "encrypted-at-rest")).toEqual(["assets", "customerData", "customersDb", "dbSecret", "ordersTable"]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("runs-as, can-assume (by reference and by ARN naming a declared role) and grants", () => {
|
|
111
|
+
expect(pairs(engine, "runs-as")).toEqual([
|
|
112
|
+
"adminBox workerRole",
|
|
113
|
+
"batch batchRole",
|
|
114
|
+
"reporter reporterRole",
|
|
115
|
+
"webServer webRole",
|
|
116
|
+
"worker workerRole",
|
|
117
|
+
]);
|
|
118
|
+
expect(pairs(engine, "can-assume")).toEqual(["batchRole auditRole", "webRole deployRole"]);
|
|
119
|
+
expect(pairs(engine, "grants", ["dynamodb:Scan"])).toEqual(["auditRole ordersTable"]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("attached-to-nothing and publicly-readable hold for nothing in the ledger, and for what a small graph declares", () => {
|
|
123
|
+
expect(set(engine, "attached-to-nothing")).toEqual([]);
|
|
124
|
+
expect(set(engine, "publicly-readable")).toEqual([]);
|
|
125
|
+
const g: GraphIR = {
|
|
126
|
+
nodes: [
|
|
127
|
+
{ id: "sg", kind: "AWS::EC2::SecurityGroup", lexicon: "aws", attrs: {} },
|
|
128
|
+
{ id: "open", kind: "AWS::S3::Bucket", lexicon: "aws", attrs: {} },
|
|
129
|
+
{ id: "openPolicy", kind: "AWS::S3::BucketPolicy", lexicon: "aws", attrs: { Bucket: { $ref: "open" }, PolicyDocument: { Statement: [{ Effect: "Allow", Principal: "*", Action: "s3:GetObject" }] } } },
|
|
130
|
+
{ id: "blocked", kind: "AWS::S3::Bucket", lexicon: "aws", attrs: { PublicAccessBlockConfiguration: { BlockPublicPolicy: true } } },
|
|
131
|
+
{ id: "blockedPolicy", kind: "AWS::S3::BucketPolicy", lexicon: "aws", attrs: { Bucket: { $ref: "blocked" }, PolicyDocument: { Statement: [{ Effect: "Allow", Principal: { AWS: "*" }, Action: "s3:GetObject" }] } } },
|
|
132
|
+
] as never,
|
|
133
|
+
edges: [
|
|
134
|
+
{ from: "openPolicy", to: "open", viaAttr: "Bucket" },
|
|
135
|
+
{ from: "blockedPolicy", to: "blocked", viaAttr: "Bucket" },
|
|
136
|
+
] as never,
|
|
137
|
+
groups: {},
|
|
138
|
+
};
|
|
139
|
+
const e = engineFor(g);
|
|
140
|
+
expect(set(e, "attached-to-nothing")).toEqual(["sg"]);
|
|
141
|
+
expect(set(e, "publicly-readable")).toEqual(["open"]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("in-default-vpc reads a live VPC's IsDefault, through the instance's VpcId or its subnet", () => {
|
|
145
|
+
const g: GraphIR = {
|
|
146
|
+
nodes: [
|
|
147
|
+
{ id: "east::vpc-default", kind: "AWS::EC2::VPC", lexicon: "aws", attrs: { IsDefault: true } },
|
|
148
|
+
{ id: "east::vpc-main", kind: "AWS::EC2::VPC", lexicon: "aws", attrs: { IsDefault: false } },
|
|
149
|
+
{ id: "east::subnet-a", kind: "AWS::EC2::Subnet", lexicon: "aws", attrs: {} },
|
|
150
|
+
{ id: "east::i-old", kind: "AWS::EC2::Instance", lexicon: "aws", attrs: {} },
|
|
151
|
+
{ id: "east::i-new", kind: "AWS::EC2::Instance", lexicon: "aws", attrs: {} },
|
|
152
|
+
{ id: "east::i-sub", kind: "AWS::EC2::Instance", lexicon: "aws", attrs: {} },
|
|
153
|
+
] as never,
|
|
154
|
+
edges: [
|
|
155
|
+
{ from: "east::i-old", to: "east::vpc-default", viaAttr: "VpcId" },
|
|
156
|
+
{ from: "east::i-new", to: "east::vpc-main", viaAttr: "VpcId" },
|
|
157
|
+
{ from: "east::i-sub", to: "east::subnet-a", viaAttr: "SubnetId" },
|
|
158
|
+
{ from: "east::subnet-a", to: "east::vpc-default", viaAttr: "VpcId" },
|
|
159
|
+
] as never,
|
|
160
|
+
groups: {},
|
|
161
|
+
};
|
|
162
|
+
expect(set(engineFor(g), "in-default-vpc")).toEqual(["i-old", "i-sub", "subnet-a"]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("the aws mapping supplies every capability", () => {
|
|
166
|
+
for (const rule of shippedRules().rules.filter(isPublic)) {
|
|
167
|
+
expect(lexiconSupport(rule, awsCapabilityPack.mapping, "aws"), rule.name).toBe("supported");
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("the effective-topology fold over the same graph", () => {
|
|
173
|
+
it("reads the standalone ingress resource too, naming its source group", () => {
|
|
174
|
+
const enriched = enrichEffectiveTopology(ir);
|
|
175
|
+
const attrs = (id: string) => enriched.nodes.find((n) => n.id === id)!.attrs as Record<string, unknown>;
|
|
176
|
+
expect(attrs("webServer").effectiveIngress).toEqual(["tcp:443:0.0.0.0/0", "tcp:22:0.0.0.0/0"]);
|
|
177
|
+
expect(attrs("webServer").internetFacing).toBe(true);
|
|
178
|
+
expect(attrs("webServer").internetFacingVia).toBe("publicRt → igw");
|
|
179
|
+
expect(attrs("worker").effectiveIngress).toEqual(["tcp:8080:sg:webSg"]);
|
|
180
|
+
expect(attrs("worker").internetFacing).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A reference inside a CloudFormation `Sub` is a graph edge (chant #4373).
|
|
3
|
+
*
|
|
4
|
+
* The estate is the shape #4361 measured on: an IAM policy granting
|
|
5
|
+
* `${bucket.Arn}/*` and a database whose password is a
|
|
6
|
+
* `{{resolve:secretsmanager:${Ref(secret)}}}` reference. Before #4373 both
|
|
7
|
+
* were `{ "$intrinsic": true }` with no edge, so a search walking from the
|
|
8
|
+
* policy to a bucket found nothing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
13
|
+
import { resolveAttrRefs } from "@intentius/chant/discovery/resolve";
|
|
14
|
+
import { buildGraphIr, type GraphIR } from "@intentius/chant/graph-ir";
|
|
15
|
+
import { applyDetail } from "@intentius/chant/graph-detail";
|
|
16
|
+
import { __searchInternals, resolveKindTerms } from "@intentius/chant/cli/handlers/search";
|
|
17
|
+
import { Bucket, DbInstance, Policy, Ref, Role, Secret, Sub, AWS } from "./index";
|
|
18
|
+
|
|
19
|
+
const { parseQuery, matchTerm, explain } = __searchInternals;
|
|
20
|
+
|
|
21
|
+
function estate(): GraphIR {
|
|
22
|
+
const assets = new Bucket({});
|
|
23
|
+
const webRole = new Role({ AssumeRolePolicyDocument: { Statement: [] } });
|
|
24
|
+
const dbSecret = new Secret({});
|
|
25
|
+
const webPolicy = new Policy({
|
|
26
|
+
PolicyName: "web",
|
|
27
|
+
Roles: [Ref(webRole)],
|
|
28
|
+
PolicyDocument: {
|
|
29
|
+
Statement: [
|
|
30
|
+
{ Effect: "Allow", Action: "s3:GetObject", Resource: Sub`${assets.Arn}/*` },
|
|
31
|
+
{ Effect: "Allow", Action: "logs:PutLogEvents", Resource: Sub`arn:aws:logs:${AWS.Region}:${AWS.AccountId}:*` },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const customersDb = new DbInstance({
|
|
36
|
+
Engine: "postgres",
|
|
37
|
+
DBInstanceClass: "db.t3.micro",
|
|
38
|
+
MasterUserPassword: Sub`{{resolve:secretsmanager:${Ref(dbSecret)}}}`,
|
|
39
|
+
});
|
|
40
|
+
const entities = new Map<string, Declarable>([
|
|
41
|
+
["assets", assets],
|
|
42
|
+
["webRole", webRole],
|
|
43
|
+
["dbSecret", dbSecret],
|
|
44
|
+
["webPolicy", webPolicy],
|
|
45
|
+
["customersDb", customersDb],
|
|
46
|
+
] as Array<[string, Declarable]>);
|
|
47
|
+
resolveAttrRefs(entities);
|
|
48
|
+
return buildGraphIr(entities);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function search(ir: GraphIR, query: string): string[] {
|
|
52
|
+
const terms = parseQuery(query);
|
|
53
|
+
resolveKindTerms(terms, ir.nodes);
|
|
54
|
+
const byId = new Map(ir.nodes.map((n) => [n.id, n]));
|
|
55
|
+
return ir.nodes.filter((n) => terms.every((t) => matchTerm(n, t, ir, byId))).map((n) => n.id).sort();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("a Sub reference in an aws project (#4373)", () => {
|
|
59
|
+
afterEach(() => vi.restoreAllMocks());
|
|
60
|
+
|
|
61
|
+
it("is an edge from the holder through its top-level attribute, at tiers 1 to 3", () => {
|
|
62
|
+
const ir = estate();
|
|
63
|
+
for (const tier of [1, 2, 3] as const) {
|
|
64
|
+
const edges = applyDetail(ir, tier).edges.map((e) => `${e.from} -${e.viaAttr}-> ${e.to}`);
|
|
65
|
+
expect(edges).toContain("webPolicy -PolicyDocument-> assets");
|
|
66
|
+
expect(edges).toContain("customersDb -MasterUserPassword-> dbSecret");
|
|
67
|
+
}
|
|
68
|
+
const t3 = applyDetail(ir, 3).edges.find((e) => e.from === "webPolicy" && e.to === "assets");
|
|
69
|
+
expect(t3).toEqual({ from: "webPolicy", to: "assets", kind: "ref", viaAttr: "PolicyDocument", toAttr: "Arn" });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("carries the template in a $sub envelope at --detail 3", () => {
|
|
73
|
+
const ir = applyDetail(estate(), 3);
|
|
74
|
+
const policy = ir.nodes.find((n) => n.id === "webPolicy")!;
|
|
75
|
+
const statements = (policy.attrs.PolicyDocument as { Statement: Array<{ Resource: unknown }> }).Statement;
|
|
76
|
+
expect(statements[0].Resource).toEqual({ $sub: "${assets.Arn}/*" });
|
|
77
|
+
expect(statements[1].Resource).toEqual({ $sub: "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*" });
|
|
78
|
+
const db = ir.nodes.find((n) => n.id === "customersDb")!;
|
|
79
|
+
expect(db.attrs.MasterUserPassword).toEqual({ $sub: "{{resolve:secretsmanager:${dbSecret}}}" });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("a pseudo-parameter-only Sub adds no edge", () => {
|
|
83
|
+
const fromPolicy = estate().edges.filter((e) => e.from === "webPolicy").map((e) => e.to).sort();
|
|
84
|
+
expect(fromPolicy).toEqual(["assets", "webRole"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('search "->*kind:AWS::S3::Bucket" follows the edge, and --explain prints it', () => {
|
|
88
|
+
const ir = estate();
|
|
89
|
+
expect(search(ir, "->*kind:AWS::S3::Bucket")).toEqual(["webPolicy"]);
|
|
90
|
+
|
|
91
|
+
const lines: string[] = [];
|
|
92
|
+
vi.spyOn(console, "log").mockImplementation((line: unknown) => void lines.push(String(line)));
|
|
93
|
+
const query = "->*kind:AWS::S3::Bucket";
|
|
94
|
+
const terms = parseQuery(query);
|
|
95
|
+
resolveKindTerms(terms, ir.nodes);
|
|
96
|
+
const byId = new Map(ir.nodes.map((n) => [n.id, n]));
|
|
97
|
+
const matches = ir.nodes.filter((n) => terms.every((t) => matchTerm(n, t, ir, byId)));
|
|
98
|
+
explain(terms, matches, ir, byId, query);
|
|
99
|
+
expect(lines.join("\n")).toContain("path webPolicy.PolicyDocument -> assets");
|
|
100
|
+
});
|
|
101
|
+
});
|
package/src/plugin.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { compositeCatalog } from "./composites/catalog";
|
|
|
2
2
|
import { createRequire } from "module";
|
|
3
3
|
import { awsBehaviourKinds } from "./behaviour-kinds";
|
|
4
4
|
import { detectTemplate } from "./detect";
|
|
5
|
+
import { awsCapabilityPack } from "./capabilities/pack";
|
|
5
6
|
import type { LexiconPlugin, IntrinsicDef, ObservationResult, DeepObservationResult, DependencyObservation, DescribeIdentityOptions, DescribeIdentityResult, DisruptionQuery, DisruptionVerdict, ResourceMetadata, ExportedTemplate, ResourceSelector, InitTemplateSet, StackStatusObservation } from "@intentius/chant/lexicon";
|
|
6
7
|
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
7
8
|
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
@@ -79,6 +80,7 @@ export const awsPlugin: LexiconPlugin = {
|
|
|
79
80
|
auditCatalog: () => awsAuditCatalog,
|
|
80
81
|
// Live edge reconstruction for `chant graph --live` (#778).
|
|
81
82
|
referenceCatalog: awsReferenceCatalog,
|
|
83
|
+
capabilityPack: awsCapabilityPack,
|
|
82
84
|
|
|
83
85
|
lintRules(): LintRule[] {
|
|
84
86
|
const rulesDir = join(dirname(fileURLToPath(import.meta.url)), "lint", "rules");
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// "ledger": the CloudFormation form of the estate chant#4361 measured the rule language on (#4375).
|
|
2
|
+
import {
|
|
3
|
+
Bucket, DbInstance, EC2Route, EC2SecurityGroupIngress, Function, Instance, InstanceProfile,
|
|
4
|
+
InternetGateway, LaunchTemplate, ManagedPolicy, Policy, Role, RouteTable, S3BucketPolicy, Secret,
|
|
5
|
+
SecurityGroup, Subnet, SubnetRouteTableAssociation, Table, Url, Vpc, VPCGatewayAttachment, Ref, Sub,
|
|
6
|
+
} from "@intentius/chant-lexicon-aws";
|
|
7
|
+
|
|
8
|
+
const AMI = "ami-0f3f13f145e66a0a3";
|
|
9
|
+
const restricted = [{ Key: "data-classification", Value: "restricted" }];
|
|
10
|
+
const trustService = (svc: string) => ({
|
|
11
|
+
Version: "2012-10-17",
|
|
12
|
+
Statement: [{ Effect: "Allow", Principal: { Service: svc }, Action: "sts:AssumeRole" }],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// network
|
|
16
|
+
export const vpc = new Vpc({ CidrBlock: "10.0.0.0/16" });
|
|
17
|
+
export const publicSubnet = new Subnet({ VpcId: vpc.VpcId, CidrBlock: "10.0.0.0/24" });
|
|
18
|
+
export const privateSubnet = new Subnet({ VpcId: vpc.VpcId, CidrBlock: "10.0.1.0/24" });
|
|
19
|
+
export const igw = new InternetGateway({});
|
|
20
|
+
export const igwAttachment = new VPCGatewayAttachment({ VpcId: vpc.VpcId, InternetGatewayId: igw.InternetGatewayId });
|
|
21
|
+
export const publicRt = new RouteTable({ VpcId: vpc.VpcId });
|
|
22
|
+
export const privateRt = new RouteTable({ VpcId: vpc.VpcId });
|
|
23
|
+
export const publicDefault = new EC2Route({ RouteTableId: publicRt.RouteTableId, DestinationCidrBlock: "0.0.0.0/0", GatewayId: igw.InternetGatewayId });
|
|
24
|
+
export const publicAssoc = new SubnetRouteTableAssociation({ RouteTableId: publicRt.RouteTableId, SubnetId: publicSubnet.SubnetId });
|
|
25
|
+
export const privateAssoc = new SubnetRouteTableAssociation({ RouteTableId: privateRt.RouteTableId, SubnetId: privateSubnet.SubnetId });
|
|
26
|
+
|
|
27
|
+
export const webSg = new SecurityGroup({
|
|
28
|
+
GroupDescription: "web", VpcId: vpc.VpcId,
|
|
29
|
+
SecurityGroupIngress: [
|
|
30
|
+
{ IpProtocol: "tcp", FromPort: 443, ToPort: 443, CidrIp: "0.0.0.0/0" },
|
|
31
|
+
{ IpProtocol: "tcp", FromPort: 22, ToPort: 22, CidrIp: "0.0.0.0/0" },
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
export const appSg = new SecurityGroup({ GroupDescription: "app", VpcId: vpc.VpcId });
|
|
35
|
+
export const appFromWeb = new EC2SecurityGroupIngress({
|
|
36
|
+
GroupId: appSg.GroupId, IpProtocol: "tcp", FromPort: 8080, ToPort: 8080, SourceSecurityGroupId: webSg.GroupId,
|
|
37
|
+
});
|
|
38
|
+
export const dbSg = new SecurityGroup({
|
|
39
|
+
GroupDescription: "db", VpcId: vpc.VpcId,
|
|
40
|
+
SecurityGroupIngress: [{ IpProtocol: "tcp", FromPort: 5432, ToPort: 5432, SourceSecurityGroupId: appSg.GroupId }],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// data
|
|
44
|
+
export const assets = new Bucket({ PublicAccessBlockConfiguration: { BlockPublicAcls: true, BlockPublicPolicy: true, IgnorePublicAcls: true, RestrictPublicBuckets: true } });
|
|
45
|
+
export const customerData = new Bucket({ Tags: restricted, PublicAccessBlockConfiguration: { BlockPublicAcls: true, BlockPublicPolicy: true, IgnorePublicAcls: true, RestrictPublicBuckets: true } });
|
|
46
|
+
export const ordersTable = new Table({
|
|
47
|
+
KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
|
|
48
|
+
AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }],
|
|
49
|
+
BillingMode: "PAY_PER_REQUEST",
|
|
50
|
+
});
|
|
51
|
+
export const dbSecret = new Secret({ GenerateSecretString: { PasswordLength: 32 } });
|
|
52
|
+
export const customersDb = new DbInstance({
|
|
53
|
+
Engine: "postgres", DBInstanceClass: "db.t3.micro", AllocatedStorage: "20",
|
|
54
|
+
MasterUsername: "ledger", MasterUserPassword: Sub`{{resolve:secretsmanager:${Ref(dbSecret)}}}`,
|
|
55
|
+
VPCSecurityGroups: [dbSg.GroupId], PubliclyAccessible: false, Tags: restricted, StorageEncrypted: true, BackupRetentionPeriod: 7,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// identity
|
|
59
|
+
export const webRole = new Role({ AssumeRolePolicyDocument: trustService("ec2.amazonaws.com") });
|
|
60
|
+
export const deployRole = new Role({
|
|
61
|
+
AssumeRolePolicyDocument: {
|
|
62
|
+
Version: "2012-10-17",
|
|
63
|
+
Statement: [{ Effect: "Allow", Principal: { AWS: webRole.Arn }, Action: "sts:AssumeRole" }],
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
export const webPolicy = new Policy({
|
|
67
|
+
PolicyName: "web", Roles: [Ref(webRole)],
|
|
68
|
+
PolicyDocument: {
|
|
69
|
+
Version: "2012-10-17",
|
|
70
|
+
Statement: [
|
|
71
|
+
{ Effect: "Allow", Action: "s3:GetObject", Resource: Sub`${assets.Arn}/*` },
|
|
72
|
+
{ Effect: "Allow", Action: "sts:AssumeRole", Resource: deployRole.Arn },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
export const deployPolicy = new Policy({
|
|
77
|
+
PolicyName: "deploy", Roles: [Ref(deployRole)],
|
|
78
|
+
PolicyDocument: {
|
|
79
|
+
Version: "2012-10-17",
|
|
80
|
+
Statement: [
|
|
81
|
+
{ Effect: "Allow", Action: "s3:*", Resource: [customerData.Arn, Sub`${customerData.Arn}/*`] },
|
|
82
|
+
{ Effect: "Allow", Action: "secretsmanager:GetSecretValue", Resource: Ref(dbSecret) },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
export const workerRole = new Role({
|
|
87
|
+
AssumeRolePolicyDocument: trustService("ec2.amazonaws.com"),
|
|
88
|
+
Policies: [{
|
|
89
|
+
PolicyName: "worker",
|
|
90
|
+
PolicyDocument: {
|
|
91
|
+
Version: "2012-10-17",
|
|
92
|
+
Statement: [
|
|
93
|
+
{ Effect: "Allow", Action: "secretsmanager:GetSecretValue", Resource: Ref(dbSecret) },
|
|
94
|
+
{ Effect: "Allow", Action: ["dynamodb:GetItem", "dynamodb:PutItem"], Resource: ordersTable.Arn },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
}],
|
|
98
|
+
});
|
|
99
|
+
export const reporterRole = new Role({ AssumeRolePolicyDocument: trustService("lambda.amazonaws.com") });
|
|
100
|
+
export const reportRead = new ManagedPolicy({
|
|
101
|
+
Roles: [Ref(reporterRole)],
|
|
102
|
+
PolicyDocument: {
|
|
103
|
+
Version: "2012-10-17",
|
|
104
|
+
Statement: [
|
|
105
|
+
{ Effect: "Allow", Action: "s3:GetObject", Resource: Sub`${customerData.Arn}/*` },
|
|
106
|
+
{ Effect: "Allow", Action: "logs:*", Resource: "*" },
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
export const batchRole = new Role({
|
|
111
|
+
AssumeRolePolicyDocument: trustService("lambda.amazonaws.com"),
|
|
112
|
+
Policies: [{
|
|
113
|
+
PolicyName: "batch",
|
|
114
|
+
PolicyDocument: { Version: "2012-10-17", Statement: [{ Effect: "Allow", Action: "sts:AssumeRole", Resource: "arn:aws:iam::123456789012:role/audit" }] },
|
|
115
|
+
}],
|
|
116
|
+
});
|
|
117
|
+
export const auditRole = new Role({
|
|
118
|
+
RoleName: "audit",
|
|
119
|
+
AssumeRolePolicyDocument: {
|
|
120
|
+
Version: "2012-10-17",
|
|
121
|
+
Statement: [{ Effect: "Allow", Principal: { AWS: batchRole.Arn }, Action: "sts:AssumeRole" }],
|
|
122
|
+
},
|
|
123
|
+
Policies: [{
|
|
124
|
+
PolicyName: "audit",
|
|
125
|
+
PolicyDocument: { Version: "2012-10-17", Statement: [{ Effect: "Allow", Action: "dynamodb:Scan", Resource: ordersTable.Arn }] },
|
|
126
|
+
}],
|
|
127
|
+
});
|
|
128
|
+
export const assetsPolicy = new S3BucketPolicy({
|
|
129
|
+
Bucket: Ref(assets),
|
|
130
|
+
PolicyDocument: {
|
|
131
|
+
Version: "2012-10-17",
|
|
132
|
+
Statement: [{ Effect: "Allow", Principal: { AWS: workerRole.Arn }, Action: "s3:GetObject", Resource: Sub`${assets.Arn}/*` }],
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// compute
|
|
137
|
+
export const webProfile = new InstanceProfile({ Roles: [Ref(webRole)] });
|
|
138
|
+
export const workerProfile = new InstanceProfile({ Roles: [Ref(workerRole)] });
|
|
139
|
+
export const webTemplate = new LaunchTemplate({
|
|
140
|
+
LaunchTemplateData: { InstanceType: "t3.micro", ImageId: AMI, SecurityGroupIds: [webSg.GroupId] },
|
|
141
|
+
});
|
|
142
|
+
export const webServer = new Instance({
|
|
143
|
+
LaunchTemplate: { LaunchTemplateId: webTemplate.LaunchTemplateId, Version: "1" },
|
|
144
|
+
SubnetId: publicSubnet.SubnetId, IamInstanceProfile: Ref(webProfile),
|
|
145
|
+
});
|
|
146
|
+
export const worker = new Instance({
|
|
147
|
+
ImageId: AMI, InstanceType: "t3.micro", SubnetId: privateSubnet.SubnetId,
|
|
148
|
+
SecurityGroupIds: [appSg.GroupId], IamInstanceProfile: Ref(workerProfile),
|
|
149
|
+
});
|
|
150
|
+
// in a public subnet, but nothing on the internet is allowed in
|
|
151
|
+
export const adminBox = new Instance({
|
|
152
|
+
ImageId: AMI, InstanceType: "t3.micro", SubnetId: publicSubnet.SubnetId,
|
|
153
|
+
SecurityGroupIds: [appSg.GroupId], IamInstanceProfile: Ref(workerProfile),
|
|
154
|
+
});
|
|
155
|
+
export const reporter = new Function({
|
|
156
|
+
Runtime: "nodejs22.x", Handler: "index.handler", Role: reporterRole.Arn, Code: { ZipFile: "exports.handler=async()=>1" },
|
|
157
|
+
});
|
|
158
|
+
export const reporterUrl = new Url({ TargetFunctionArn: reporter.Arn, AuthType: "NONE" });
|
|
159
|
+
export const batch = new Function({
|
|
160
|
+
Runtime: "nodejs22.x", Handler: "index.handler", Role: batchRole.Arn, Code: { ZipFile: "exports.handler=async()=>1" },
|
|
161
|
+
});
|