1import * as AWS from "@/AWS";
2import { CisScanConfiguration } from "@/AWS/Inspector2/CisScanConfiguration.ts";
3import { Filter } from "@/AWS/Inspector2/Filter.ts";
4import * as Provider from "@/Provider";
5import * as Test from "@/Test/Alchemy";
6import * as inspector2 from "@distilled.cloud/aws/inspector2";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import * as Result from "effect/Result";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13// The findings-filter APIs work regardless of Inspector enablement, so the
14// Filter lifecycle always runs live.
15test.provider(
16 "lifecycle: findings filter create, update, destroy",
17 (stack) =>
18 Effect.gen(function* () {
19 yield* stack.destroy();
20
21 // Self-heal: an interrupted previous run can orphan the live filter
22 // (state lost, so the cloud resource is unadoptable under a fresh
23 // stage). Delete any filter this test's logical id created before.
24 const orphans = (yield* inspector2.listFilters({})).filters.filter(
25 (f) => f.tags?.["alchemy::id"] === "SuppressInfo",
26 );
27 yield* Effect.forEach(orphans, (f) =>
28 inspector2
29 .deleteFilter({ arn: f.arn })
30 .pipe(
31 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
32 ),
33 );
34
35 const deploy = (props: { action: "NONE" | "SUPPRESS"; reason: string }) =>
36 stack.deploy(
37 Effect.gen(function* () {
38 const filter = yield* Filter("SuppressInfo", {
39 action: props.action,
40 reason: props.reason,
41 description: "created by alchemy Inspector2 resource test",
42 filterCriteria: {
43 severity: [{ comparison: "EQUALS", value: "INFORMATIONAL" }],
44 },
45 tags: { env: "test" },
46 });
47 return {
48 arn: filter.arn,
49 name: filter.name,
50 action: filter.action,
51 reason: filter.reason,
52 };
53 }),
54 );
55
56 // Create.
57 const created = yield* deploy({
58 action: "SUPPRESS",
59 reason: "informational findings are tracked elsewhere",
60 });
61 expect(created.arn).toContain("/filter/");
62 expect(created.action).toBe("SUPPRESS");
63
64 // Out-of-band verification via distilled.
65 const live = (yield* inspector2.listFilters({ arns: [created.arn] }))
66 .filters[0];
67 expect(live?.action).toBe("SUPPRESS");
68 expect(live?.tags?.["env"]).toBe("test");
69 expect(live?.tags?.["alchemy::id"]).toBe("SuppressInfo");
70
71 // Canonical list() coverage.
72 const provider = yield* Provider.findProvider(Filter);
73 const all = yield* provider.list();
74 expect(all.some((f) => f.arn === created.arn)).toBe(true);
75
76 // Update in place — the ARN is stable.
77 const updated = yield* deploy({
78 action: "NONE",
79 reason: "keep them visible after all",
80 });
81 expect(updated.arn).toBe(created.arn);
82 expect(updated.action).toBe("NONE");
83 const liveUpdated = (yield* inspector2.listFilters({
84 arns: [created.arn],
85 })).filters[0];
86 expect(liveUpdated?.action).toBe("NONE");
87 expect(liveUpdated?.reason).toBe("keep them visible after all");
88
89 // Destroy — the filter is gone.
90 yield* stack.destroy();
91 const gone = yield* inspector2.listFilters({ arns: [created.arn] });
92 expect(gone.filters).toHaveLength(0);
93 }),
94 {
95 tags: ["provider:aws", "provider:aws:inspector2", "live"],
96 timeout: 120_000,
97 },
98);
99
100// The CIS scan APIs are hard-gated on Inspector enablement — a disabled
101// account gets a typed AccessDeniedException ("Invoking account is not
102// enabled."). This ungated probe pins that behavior; the full lifecycle
103// below only runs against an Inspector-enabled account.
104test.provider(
105 "CIS scan APIs reject a non-enabled account (typed)",
106 () =>
107 Effect.gen(function* () {
108 const account = (yield* inspector2.batchGetAccountStatus({}))
109 .accounts?.[0];
110 if (account?.state?.status === "ENABLED") {
111 yield* Effect.logInfo(
112 "Inspector is enabled in this account — CIS APIs are accessible, probe not applicable",
113 );
114 return;
115 }
116 const result = yield* Effect.result(
117 inspector2.listCisScanConfigurations({}),
118 );
119 expect(Result.isFailure(result)).toBe(true);
120 if (Result.isFailure(result)) {
121 expect(result.failure._tag).toBe("AccessDeniedException");
122 }
123 }),
124 { tags: ["provider:aws", "provider:aws:inspector2", "live"] },
125);
126
127// Full CIS scan configuration lifecycle — requires Inspector to be enabled
128// (INSPECTOR2_TEST_CIS=1 on an enabled account).
129test.provider.skipIf(!process.env.INSPECTOR2_TEST_CIS)(
130 "lifecycle: CIS scan configuration create, update, destroy",
131 (stack) =>
132 Effect.gen(function* () {
133 yield* stack.destroy();
134
135 const deploy = (props: {
136 securityLevel: "LEVEL_1" | "LEVEL_2";
137 timeOfDay: string;
138 }) =>
139 stack.deploy(
140 Effect.gen(function* () {
141 const cis = yield* CisScanConfiguration("NightlyCis", {
142 securityLevel: props.securityLevel,
143 schedule: {
144 daily: {
145 startTime: { timeOfDay: props.timeOfDay, timezone: "UTC" },
146 },
147 },
148 targets: {
149 accountIds: ["SELF"],
150 targetResourceTags: { AlchemyCisTest: ["true"] },
151 },
152 tags: { env: "test" },
153 });
154 return {
155 scanConfigurationArn: cis.scanConfigurationArn,
156 scanName: cis.scanName,
157 securityLevel: cis.securityLevel,
158 };
159 }),
160 );
161
162 const created = yield* deploy({
163 securityLevel: "LEVEL_1",
164 timeOfDay: "02:00",
165 });
166 expect(created.scanConfigurationArn).toContain("scan-configuration");
167 expect(created.securityLevel).toBe("LEVEL_1");
168
169 const byArn = () =>
170 inspector2
171 .listCisScanConfigurations({
172 filterCriteria: {
173 scanConfigurationArnFilters: [
174 { comparison: "EQUALS", value: created.scanConfigurationArn },
175 ],
176 },
177 })
178 .pipe(Effect.map((r) => r.scanConfigurations?.[0]));
179
180 const live = yield* byArn();
181 expect(live?.securityLevel).toBe("LEVEL_1");
182 expect(live?.schedule?.daily?.startTime.timeOfDay).toBe("02:00");
183
184 // Canonical list() coverage.
185 const provider = yield* Provider.findProvider(CisScanConfiguration);
186 const all = yield* provider.list();
187 expect(
188 all.some(
189 (c) => c.scanConfigurationArn === created.scanConfigurationArn,
190 ),
191 ).toBe(true);
192
193 // Update in place — the ARN is stable.
194 const updated = yield* deploy({
195 securityLevel: "LEVEL_2",
196 timeOfDay: "03:30",
197 });
198 expect(updated.scanConfigurationArn).toBe(created.scanConfigurationArn);
199 expect(updated.securityLevel).toBe("LEVEL_2");
200 const liveUpdated = yield* byArn();
201 expect(liveUpdated?.schedule?.daily?.startTime.timeOfDay).toBe("03:30");
202
203 // Destroy — the configuration is gone.
204 yield* stack.destroy();
205 expect(yield* byArn()).toBeUndefined();
206 }),
207 {
208 tags: ["provider:aws", "provider:aws:inspector2", "live"],
209 timeout: 120_000,
210 },
211);