@sagentix-inc/evidence-collector 0.3.0 → 0.3.2
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/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +14 -14
- package/apps/sos-app/src/data/checks/aws.json +2 -2
- package/apps/sos-app/src/data/checks/checkov.json +59 -59
- package/apps/sos-app/src/data/checks/kubescape.json +1 -1
- package/apps/sos-app/src/data/checks/prowler_aws.json +1926 -491
- package/apps/sos-app/src/data/checks/prowler_azure.json +634 -137
- package/apps/sos-app/src/data/checks/prowler_cloudflare.json +207 -3
- package/apps/sos-app/src/data/checks/prowler_gcp.json +274 -92
- package/apps/sos-app/src/data/checks/prowler_github.json +193 -3
- package/apps/sos-app/src/data/checks/prowler_googleworkspace.json +459 -3
- package/apps/sos-app/src/data/checks/prowler_kubernetes.json +208 -75
- package/apps/sos-app/src/data/checks/prowler_m365.json +643 -69
- package/apps/sos-app/src/data/checks/prowler_okta.json +207 -3
- package/apps/sos-app/src/data/fedramp_rules.json +1 -1
- package/collector/package.json +1 -1
- package/collector/src/sources/aws/checks.js +4 -1
- package/collector/src/sources/prowler.js +81 -26
- package/package.json +1 -1
package/collector/package.json
CHANGED
|
@@ -411,8 +411,11 @@ async function accessAnalyzer(aws, out, rr, region) {
|
|
|
411
411
|
if (!next)
|
|
412
412
|
break;
|
|
413
413
|
}
|
|
414
|
+
// Only ACCOUNT and ORGANIZATION analyzers report external access; the *_UNUSED_ACCESS and *_INTERNAL_ACCESS types
|
|
415
|
+
// analyze something else (AWS IAM Access Analyzer API Reference, AnalyzerSummary "type", read 2026-10-31).
|
|
414
416
|
const active = all.filter((a) => a.status === "ACTIVE");
|
|
415
|
-
|
|
417
|
+
const external = active.filter((a) => a.type === "ACCOUNT" || a.type === "ORGANIZATION");
|
|
418
|
+
out.add("access_analyzer_enabled", rr, external.length ? "pass" : "fail", `${all.length} analyzer(s), ${active.length} ACTIVE${active.length ? ` (types ${[...new Set(active.map((a) => String(a.type)))].join(", ")})` : ""}; one active external-access analyzer (ACCOUNT or ORGANIZATION) required`);
|
|
416
419
|
}
|
|
417
420
|
// ── Storage and encryption ──────────────────────────────────────────────────────────────────────
|
|
418
421
|
const PAB = ["BlockPublicAcls", "IgnorePublicAcls", "BlockPublicPolicy", "RestrictPublicBuckets"];
|
|
@@ -22,7 +22,7 @@ import { DEFAULT_CATALOG_DIR, loadCatalog } from "../catalog.js";
|
|
|
22
22
|
import { scrub } from "./shared-cloud.js";
|
|
23
23
|
export const PROWLER_PROVIDERS = ["aws", "azure", "gcp", "kubernetes", "m365", "github", "googleworkspace", "okta", "cloudflare"];
|
|
24
24
|
/** The version the catalogues were built from (apps/sos-app/scripts/build_prowler_catalog.py). */
|
|
25
|
-
export const PROWLER_VERSION = "5.
|
|
25
|
+
export const PROWLER_VERSION = "5.47.0";
|
|
26
26
|
/** Options that make Prowler write, fix or send, or that change the output the collector reads. */
|
|
27
27
|
export const REFUSED_ARGS = [
|
|
28
28
|
"--push-to-cloud", "--security-hub", "--send-sh-only-fails", "--fixer", "--fixer-config", "--output-bucket",
|
|
@@ -31,6 +31,7 @@ export const REFUSED_ARGS = [
|
|
|
31
31
|
];
|
|
32
32
|
/** Environment variables whose values never leave the machine, even if Prowler echoes them in a detail. */
|
|
33
33
|
const SECRET_ENVS = [
|
|
34
|
+
"GITHUB_TOKEN", "CLOUDFLARE_API_TOKEN_READONLY", "ENTRA_CLIENT_SECRET", "M365_CERTIFICATE_CONTENT",
|
|
34
35
|
"AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AZURE_CLIENT_SECRET", "GITHUB_PERSONAL_ACCESS_TOKEN", "GITHUB_OAUTH_APP_TOKEN",
|
|
35
36
|
"GITHUB_APP_KEY", "OKTA_PRIVATE_KEY", "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_KEY", "GOOGLEWORKSPACE_CREDENTIALS_CONTENT",
|
|
36
37
|
];
|
|
@@ -43,7 +44,12 @@ function catalogIds(dir = DEFAULT_CATALOG_DIR) {
|
|
|
43
44
|
return [];
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
|
-
const one = (v) =>
|
|
47
|
+
const one = (v) => {
|
|
48
|
+
const x = Array.isArray(v) ? v[0] : v;
|
|
49
|
+
return typeof x === "string" && x.trim() ? x.trim() : undefined;
|
|
50
|
+
};
|
|
51
|
+
/** --prowler-provider aws,azure or repeated, in order, without repeats. */
|
|
52
|
+
const providersOf = (v) => [...new Set((Array.isArray(v) ? v : typeof v === "string" ? [v] : []).flatMap((x) => String(x).split(",")).map((x) => x.trim()).filter(Boolean))];
|
|
47
53
|
/** Splits --prowler-args like a shell would for plain words and '…' or "…" quoting (no expansion). */
|
|
48
54
|
export function splitArgs(s) {
|
|
49
55
|
const out = [];
|
|
@@ -55,11 +61,22 @@ export function splitArgs(s) {
|
|
|
55
61
|
}
|
|
56
62
|
/** The provider's authentication flag, chosen from what the environment holds. Everything else Prowler reads from env itself. */
|
|
57
63
|
export function authArgs(provider, env) {
|
|
64
|
+
// The generated workflow names what the collector's own source reads, so Prowler reads the same scope
|
|
65
|
+
// (Prowler 5.47.0: aws --region, azure --subscription-ids, gcp --project-ids).
|
|
66
|
+
const scope = (name, flag) => { const v = (env[name] ?? "").split(/\s+/).filter(Boolean); return v.length ? [flag, ...v] : []; };
|
|
58
67
|
switch (provider) {
|
|
59
|
-
case "
|
|
68
|
+
case "aws": return scope("SAGENTIX_PROWLER_AWS_REGIONS", "--region");
|
|
69
|
+
case "gcp": return scope("SAGENTIX_PROWLER_GCP_PROJECTS", "--project-ids");
|
|
60
70
|
case "m365":
|
|
71
|
+
// A certificate signs in every Microsoft 365 service, Exchange and Teams included; a secret covers Graph only.
|
|
72
|
+
// --init-modules installs the PowerShell modules those services need (an ephemeral runner has none).
|
|
73
|
+
if (env.M365_CERTIFICATE_CONTENT)
|
|
74
|
+
return ["--certificate-auth", "--init-modules"];
|
|
75
|
+
return [...(env.AZURE_CLIENT_SECRET ? ["--sp-env-auth"] : ["--az-cli-auth"]), "--init-modules"];
|
|
76
|
+
case "azure":
|
|
61
77
|
// A client secret in env: service principal; otherwise the session `azure/login` (OIDC) left for the az CLI.
|
|
62
|
-
return env.AZURE_CLIENT_SECRET ? ["--sp-env-auth"] : ["--az-cli-auth"]
|
|
78
|
+
return [...(env.AZURE_CLIENT_SECRET ? ["--sp-env-auth"] : ["--az-cli-auth"]),
|
|
79
|
+
...(provider === "azure" ? scope("SAGENTIX_PROWLER_AZURE_SUBSCRIPTIONS", "--subscription-ids") : [])];
|
|
63
80
|
case "kubernetes":
|
|
64
81
|
return env.KUBECONFIG ? ["--kubeconfig-file", env.KUBECONFIG] : [];
|
|
65
82
|
default:
|
|
@@ -68,6 +85,25 @@ export function authArgs(provider, env) {
|
|
|
68
85
|
return [];
|
|
69
86
|
}
|
|
70
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Prowler reads its own variable names; the collector's native sources read theirs. So the same read-only credential
|
|
90
|
+
* the client already stored serves both ways (2026-11-01: each control collected at least two ways): the collector's
|
|
91
|
+
* name is copied to Prowler's when Prowler's is unset. Names from prowler/providers/<provider> in Prowler 5.47.0.
|
|
92
|
+
*/
|
|
93
|
+
export function prowlerEnv(provider, env) {
|
|
94
|
+
const alias = {
|
|
95
|
+
github: { GITHUB_PERSONAL_ACCESS_TOKEN: "GITHUB_TOKEN" },
|
|
96
|
+
cloudflare: { CLOUDFLARE_API_TOKEN: "CLOUDFLARE_API_TOKEN_READONLY" },
|
|
97
|
+
googleworkspace: { GOOGLEWORKSPACE_CREDENTIALS_FILE: "GOOGLE_SA_KEY_FILE", GOOGLEWORKSPACE_DELEGATED_USER: "GOOGLE_ADMIN_EMAIL" },
|
|
98
|
+
m365: { AZURE_TENANT_ID: "ENTRA_TENANT_ID", AZURE_CLIENT_ID: "ENTRA_CLIENT_ID", AZURE_CLIENT_SECRET: "ENTRA_CLIENT_SECRET" },
|
|
99
|
+
okta: { OKTA_ORG_DOMAIN: "OKTA_DOMAIN" },
|
|
100
|
+
};
|
|
101
|
+
const out = { ...env };
|
|
102
|
+
for (const [theirs, ours] of Object.entries(alias[provider] ?? {}))
|
|
103
|
+
if (!out[theirs] && env[ours])
|
|
104
|
+
out[theirs] = env[ours];
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
71
107
|
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
72
108
|
const s = (v) => (typeof v === "string" ? v : "");
|
|
73
109
|
const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
@@ -184,44 +220,63 @@ export async function runProwler(provider, o) {
|
|
|
184
220
|
export const prowler = {
|
|
185
221
|
name: "prowler",
|
|
186
222
|
options: {
|
|
187
|
-
"prowler-provider": { type: "string", help: `Prowler
|
|
223
|
+
"prowler-provider": { type: "string", multiple: true, help: `Prowler providers to run, comma-separated: ${PROWLER_PROVIDERS.join(", ")} (one failing never stops the others)` },
|
|
188
224
|
"prowler-bin": { type: "string", help: "the prowler executable (default prowler on PATH)" },
|
|
189
225
|
"prowler-args": { type: "string", help: "extra Prowler options, written --prowler-args=\"--region us-east-1 ca-central-1\" (never output, send or fix options)" },
|
|
190
226
|
"prowler-output": { type: "string", help: "read this json-ocsf file instead of running Prowler" },
|
|
191
227
|
},
|
|
192
228
|
emits: catalogIds(),
|
|
193
229
|
describe(opts) {
|
|
194
|
-
const p =
|
|
230
|
+
const p = providersOf(opts["prowler-provider"]).join(", ") || "(no provider)";
|
|
195
231
|
return one(opts["prowler-output"]) ? `Prowler ${p} findings from a json-ocsf file` : `Prowler ${PROWLER_VERSION} ${p} scan`;
|
|
196
232
|
},
|
|
197
233
|
async run(ctx) {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const known = new Set(catalogIds(one(ctx.opts.catalog) ?? DEFAULT_CATALOG_DIR));
|
|
203
|
-
if (![...known].some((id) => id.startsWith(`prowler.${provider}.`))) {
|
|
204
|
-
ctx.warn(`prowler: sos-app's catalogue lists no ${provider} check (Prowler maps none to a KSI or SP 800-53 control); every finding will be dropped.`);
|
|
234
|
+
const providers = providersOf(ctx.opts["prowler-provider"]);
|
|
235
|
+
const bad = providers.filter((p) => !PROWLER_PROVIDERS.includes(p));
|
|
236
|
+
if (!providers.length || bad.length) {
|
|
237
|
+
throw new Error(`--prowler-provider is required: one or more of ${PROWLER_PROVIDERS.join(", ")}${bad.length ? ` (not: ${bad.join(", ")})` : ""}.`);
|
|
205
238
|
}
|
|
206
239
|
const file = one(ctx.opts["prowler-output"]);
|
|
207
|
-
|
|
208
|
-
|
|
240
|
+
if (file && providers.length > 1)
|
|
241
|
+
throw new Error("--prowler-output reads one provider's file; name one --prowler-provider with it.");
|
|
242
|
+
const known = new Set(catalogIds(one(ctx.opts.catalog) ?? DEFAULT_CATALOG_DIR));
|
|
243
|
+
const results = [];
|
|
244
|
+
const failures = [];
|
|
245
|
+
for (const provider of providers) {
|
|
246
|
+
if (![...known].some((id) => id.startsWith(`prowler.${provider}.`))) {
|
|
247
|
+
ctx.warn(`prowler: sos-app's catalogue lists no ${provider} check; every finding will be dropped.`);
|
|
248
|
+
}
|
|
249
|
+
let text;
|
|
209
250
|
try {
|
|
210
|
-
|
|
251
|
+
if (file) {
|
|
252
|
+
try {
|
|
253
|
+
text = readFileSync(file, "utf8");
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
throw new Error(`cannot read --prowler-output ${file}: ${e.code ?? String(e)}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const spawner = ctx.clients.prowler ?? spawnProwler;
|
|
261
|
+
const extra = one(ctx.opts["prowler-args"]) ? splitArgs(one(ctx.opts["prowler-args"])) : [];
|
|
262
|
+
text = await runProwler(provider, { bin: one(ctx.opts["prowler-bin"]) ?? "prowler", extra, env: prowlerEnv(provider, ctx.env), spawner });
|
|
263
|
+
}
|
|
211
264
|
}
|
|
212
265
|
catch (e) {
|
|
213
|
-
|
|
266
|
+
// A second way that fails leaves the control to its other ways: say so and go on with the next provider.
|
|
267
|
+
failures.push(e);
|
|
268
|
+
if (providers.length > 1)
|
|
269
|
+
ctx.warn(`prowler: ${provider} did not run (${e.message}); its controls keep their other ways.`);
|
|
270
|
+
continue;
|
|
214
271
|
}
|
|
272
|
+
const { results: r, summary } = parseOcsf(text, provider, known);
|
|
273
|
+
results.push(...r);
|
|
274
|
+
ctx.warn(`prowler: ${summary.findings} findings; ${summary.kept} results (${summary.merged} merged into the same check and resource, ` +
|
|
275
|
+
`${summary.muted} muted and ${summary.manual} manual sent as skipped); ${summary.dropped} findings dropped from ${summary.droppedChecks.length} checks not in the catalogue` +
|
|
276
|
+
(summary.droppedChecks.length ? `: ${summary.droppedChecks.slice(0, 20).join(", ")}${summary.droppedChecks.length > 20 ? ", …" : ""}` : ""));
|
|
215
277
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const extra = one(ctx.opts["prowler-args"]) ? splitArgs(one(ctx.opts["prowler-args"])) : [];
|
|
219
|
-
text = await runProwler(provider, { bin: one(ctx.opts["prowler-bin"]) ?? "prowler", extra, env: ctx.env, spawner });
|
|
220
|
-
}
|
|
221
|
-
const { results, summary } = parseOcsf(text, provider, known);
|
|
222
|
-
ctx.warn(`prowler: ${summary.findings} findings; ${summary.kept} results (${summary.merged} merged into the same check and resource, ` +
|
|
223
|
-
`${summary.muted} muted and ${summary.manual} manual sent as skipped); ${summary.dropped} findings dropped from ${summary.droppedChecks.length} checks not in the catalogue` +
|
|
224
|
-
(summary.droppedChecks.length ? `: ${summary.droppedChecks.slice(0, 20).join(", ")}${summary.droppedChecks.length > 20 ? ", …" : ""}` : ""));
|
|
278
|
+
if (failures.length === providers.length)
|
|
279
|
+
throw failures[0];
|
|
225
280
|
return scrub(results, SECRET_ENVS.map((n) => ctx.env[n]));
|
|
226
281
|
},
|
|
227
282
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sagentix-inc/evidence-collector",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Customer-side evidence collector: reads systems read-only, runs checks, signs the results and pushes them outbound to sos-app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|