@cyclonedx/cdxgen 12.1.5 → 12.2.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/README.md +47 -39
- package/bin/cdxgen.js +175 -96
- package/bin/evinse.js +4 -4
- package/bin/repl.js +1 -1
- package/bin/sign.js +102 -0
- package/bin/validate.js +233 -0
- package/bin/verify.js +69 -28
- package/data/queries.json +1 -1
- package/data/rules/ci-permissions.yaml +186 -0
- package/data/rules/dependency-sources.yaml +123 -0
- package/data/rules/package-integrity.yaml +135 -0
- package/data/rules/vscode-extensions.yaml +228 -0
- package/lib/cli/index.js +327 -372
- package/lib/evinser/db.js +137 -0
- package/lib/{helpers → evinser}/db.poku.js +2 -6
- package/lib/evinser/evinser.js +2 -14
- package/lib/helpers/bomSigner.js +312 -0
- package/lib/helpers/bomSigner.poku.js +156 -0
- package/lib/helpers/ciParsers/azurePipelines.js +295 -0
- package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
- package/lib/helpers/ciParsers/circleCi.js +286 -0
- package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
- package/lib/helpers/ciParsers/common.js +24 -0
- package/lib/helpers/ciParsers/githubActions.js +636 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
- package/lib/helpers/ciParsers/gitlabCi.js +213 -0
- package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
- package/lib/helpers/ciParsers/jenkins.js +181 -0
- package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
- package/lib/helpers/depsUtils.js +203 -0
- package/lib/helpers/depsUtils.poku.js +150 -0
- package/lib/helpers/display.js +423 -4
- package/lib/helpers/envcontext.js +18 -3
- package/lib/helpers/formulationParsers.js +351 -0
- package/lib/helpers/logger.js +14 -0
- package/lib/helpers/protobom.js +9 -9
- package/lib/helpers/pythonutils.js +9 -0
- package/lib/helpers/utils.js +681 -406
- package/lib/helpers/utils.poku.js +55 -255
- package/lib/helpers/versutils.js +202 -0
- package/lib/helpers/versutils.poku.js +315 -0
- package/lib/helpers/vsixutils.js +1061 -0
- package/lib/helpers/vsixutils.poku.js +2247 -0
- package/lib/managers/binary.js +19 -19
- package/lib/managers/docker.js +108 -1
- package/lib/managers/oci.js +10 -0
- package/lib/managers/piptree.js +3 -9
- package/lib/parsers/npmrc.js +17 -13
- package/lib/parsers/npmrc.poku.js +41 -5
- package/lib/server/openapi.yaml +1 -1
- package/lib/server/server.js +40 -11
- package/lib/server/server.poku.js +123 -144
- package/lib/stages/postgen/annotator.js +1 -1
- package/lib/stages/postgen/auditBom.js +197 -0
- package/lib/stages/postgen/auditBom.poku.js +378 -0
- package/lib/stages/postgen/postgen.js +54 -1
- package/lib/stages/postgen/postgen.poku.js +90 -1
- package/lib/stages/postgen/ruleEngine.js +369 -0
- package/lib/stages/pregen/envAudit.js +299 -0
- package/lib/stages/pregen/envAudit.poku.js +572 -0
- package/lib/stages/pregen/pregen.js +12 -8
- package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
- package/lib/validator/complianceEngine.js +241 -0
- package/lib/validator/complianceEngine.poku.js +168 -0
- package/lib/validator/complianceRules.js +1610 -0
- package/lib/validator/complianceRules.poku.js +328 -0
- package/lib/validator/index.js +222 -0
- package/lib/validator/index.poku.js +144 -0
- package/lib/validator/reporters/annotations.js +121 -0
- package/lib/validator/reporters/console.js +149 -0
- package/lib/validator/reporters/index.js +41 -0
- package/lib/validator/reporters/json.js +37 -0
- package/lib/validator/reporters/sarif.js +184 -0
- package/lib/validator/reporters.poku.js +150 -0
- package/package.json +8 -8
- package/types/bin/sign.d.ts +3 -0
- package/types/bin/sign.d.ts.map +1 -0
- package/types/bin/validate.d.ts +3 -0
- package/types/bin/validate.d.ts.map +1 -0
- package/types/helpers/utils.d.ts +0 -1
- package/types/lib/cli/index.d.ts +49 -52
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/db.d.ts +34 -0
- package/types/lib/evinser/db.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +63 -16
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +27 -0
- package/types/lib/helpers/bomSigner.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/common.d.ts +11 -0
- package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts +21 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -0
- package/types/lib/helpers/display.d.ts +111 -11
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/envcontext.d.ts +19 -7
- package/types/lib/helpers/envcontext.d.ts.map +1 -1
- package/types/lib/helpers/formulationParsers.d.ts +50 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
- package/types/lib/helpers/logger.d.ts +15 -1
- package/types/lib/helpers/logger.d.ts.map +1 -1
- package/types/lib/helpers/protobom.d.ts +2 -2
- package/types/lib/helpers/pythonutils.d.ts +10 -1
- package/types/lib/helpers/pythonutils.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +532 -128
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/helpers/versutils.d.ts +8 -0
- package/types/lib/helpers/versutils.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +130 -0
- package/types/lib/helpers/vsixutils.d.ts.map +1 -0
- package/types/lib/managers/docker.d.ts +12 -31
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +11 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/managers/piptree.d.ts.map +1 -1
- package/types/lib/parsers/npmrc.d.ts +4 -1
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +21 -2
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts +20 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +8 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
- package/types/lib/stages/pregen/envAudit.d.ts +8 -0
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -0
- package/types/lib/validator/complianceEngine.d.ts +66 -0
- package/types/lib/validator/complianceEngine.d.ts.map +1 -0
- package/types/lib/validator/complianceRules.d.ts +70 -0
- package/types/lib/validator/complianceRules.d.ts.map +1 -0
- package/types/lib/validator/index.d.ts +70 -0
- package/types/lib/validator/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/annotations.d.ts +31 -0
- package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
- package/types/lib/validator/reporters/console.d.ts +30 -0
- package/types/lib/validator/reporters/console.d.ts.map +1 -0
- package/types/lib/validator/reporters/index.d.ts +21 -0
- package/types/lib/validator/reporters/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/json.d.ts +11 -0
- package/types/lib/validator/reporters/json.d.ts.map +1 -0
- package/types/lib/validator/reporters/sarif.d.ts +16 -0
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
- package/lib/helpers/db.js +0 -162
- package/lib/stages/pregen/env-audit.js +0 -34
- package/lib/stages/pregen/env-audit.poku.js +0 -290
- package/types/helpers/db.d.ts +0 -35
- package/types/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/db.d.ts +0 -35
- package/types/lib/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/validator.d.ts.map +0 -1
- package/types/lib/stages/pregen/env-audit.d.ts +0 -2
- package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
- package/types/managers/binary.d.ts +0 -37
- package/types/managers/binary.d.ts.map +0 -1
- package/types/managers/docker.d.ts +0 -56
- package/types/managers/docker.d.ts.map +0 -1
- package/types/managers/oci.d.ts +0 -2
- package/types/managers/oci.d.ts.map +0 -1
- package/types/managers/piptree.d.ts +0 -2
- package/types/managers/piptree.d.ts.map +0 -1
- package/types/server/server.d.ts +0 -34
- package/types/server/server.d.ts.map +0 -1
- package/types/stages/postgen/annotator.d.ts +0 -27
- package/types/stages/postgen/annotator.d.ts.map +0 -1
- package/types/stages/postgen/postgen.d.ts +0 -51
- package/types/stages/postgen/postgen.d.ts.map +0 -1
- package/types/stages/pregen/pregen.d.ts +0 -59
- package/types/stages/pregen/pregen.d.ts.map +0 -1
|
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
|
|
3
3
|
import { assert, it } from "poku";
|
|
4
4
|
|
|
5
|
-
import { filterBom } from "./postgen.js";
|
|
5
|
+
import { filterBom, postProcess } from "./postgen.js";
|
|
6
6
|
|
|
7
7
|
it("filter bom tests", () => {
|
|
8
8
|
const bomJson = JSON.parse(
|
|
@@ -69,3 +69,92 @@ it("filter bom tests2", () => {
|
|
|
69
69
|
},
|
|
70
70
|
]);
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
it("postProcess adds formulation exactly once when includeFormulation is true", () => {
|
|
74
|
+
const bomNSData = {
|
|
75
|
+
bomJson: {
|
|
76
|
+
bomFormat: "CycloneDX",
|
|
77
|
+
specVersion: "1.5",
|
|
78
|
+
components: [],
|
|
79
|
+
dependencies: [],
|
|
80
|
+
metadata: { properties: [] },
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const options = { includeFormulation: true, specVersion: 1.5 };
|
|
84
|
+
const result = postProcess(bomNSData, options);
|
|
85
|
+
assert.ok(
|
|
86
|
+
Array.isArray(result.bomJson.formulation),
|
|
87
|
+
"formulation must be an array",
|
|
88
|
+
);
|
|
89
|
+
assert.ok(
|
|
90
|
+
result.bomJson.formulation.length > 0,
|
|
91
|
+
"formulation must have at least one entry",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("postProcess does not add formulation when includeFormulation is false", () => {
|
|
96
|
+
const bomNSData = {
|
|
97
|
+
bomJson: {
|
|
98
|
+
bomFormat: "CycloneDX",
|
|
99
|
+
specVersion: "1.5",
|
|
100
|
+
components: [],
|
|
101
|
+
dependencies: [],
|
|
102
|
+
metadata: { properties: [] },
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
const options = { includeFormulation: false, specVersion: 1.5 };
|
|
106
|
+
const result = postProcess(bomNSData, options);
|
|
107
|
+
assert.strictEqual(
|
|
108
|
+
result.bomJson.formulation,
|
|
109
|
+
undefined,
|
|
110
|
+
"formulation must not be added when disabled",
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("postProcess preserves existing formulation and does not overwrite it", () => {
|
|
115
|
+
const sentinel = [{ "bom-ref": "already-present" }];
|
|
116
|
+
const bomNSData = {
|
|
117
|
+
bomJson: {
|
|
118
|
+
bomFormat: "CycloneDX",
|
|
119
|
+
specVersion: "1.5",
|
|
120
|
+
components: [],
|
|
121
|
+
dependencies: [],
|
|
122
|
+
metadata: { properties: [] },
|
|
123
|
+
formulation: sentinel,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
const options = { includeFormulation: true, specVersion: 1.5 };
|
|
127
|
+
const result = postProcess(bomNSData, options);
|
|
128
|
+
assert.strictEqual(
|
|
129
|
+
result.bomJson.formulation[0]["bom-ref"],
|
|
130
|
+
"already-present",
|
|
131
|
+
"existing formulation must not be overwritten",
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("postProcess passes formulationList from bomNSData into the formulation section", () => {
|
|
136
|
+
const bomNSData = {
|
|
137
|
+
bomJson: {
|
|
138
|
+
bomFormat: "CycloneDX",
|
|
139
|
+
specVersion: "1.5",
|
|
140
|
+
components: [],
|
|
141
|
+
dependencies: [],
|
|
142
|
+
metadata: { properties: [] },
|
|
143
|
+
},
|
|
144
|
+
formulationList: [{ type: "library", name: "pixi-pkg", version: "1.0.0" }],
|
|
145
|
+
};
|
|
146
|
+
const options = { includeFormulation: true, specVersion: 1.5 };
|
|
147
|
+
const result = postProcess(bomNSData, options);
|
|
148
|
+
assert.ok(
|
|
149
|
+
Array.isArray(result.bomJson.formulation),
|
|
150
|
+
"formulation must be present",
|
|
151
|
+
);
|
|
152
|
+
// The formulationList item should be reflected somewhere in the formulation components
|
|
153
|
+
const allComponents = result.bomJson.formulation.flatMap(
|
|
154
|
+
(f) => f.components ?? [],
|
|
155
|
+
);
|
|
156
|
+
assert.ok(
|
|
157
|
+
allComponents.some((c) => c.name === "pixi-pkg"),
|
|
158
|
+
"pixi-pkg from formulationList should appear in formulation components",
|
|
159
|
+
);
|
|
160
|
+
});
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONata-powered rule engine for audits
|
|
3
|
+
* Loads YAML rules and evaluates them against CycloneDX BOMs
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
import { parse as loadYaml } from "yaml";
|
|
9
|
+
|
|
10
|
+
import { DEBUG_MODE, safeExistsSync } from "../../helpers/utils.js";
|
|
11
|
+
|
|
12
|
+
let jsonata;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
({ default: jsonata } = await import("jsonata"));
|
|
16
|
+
} catch {
|
|
17
|
+
jsonata = () => {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"BOM audit rule evaluation requires the optional `jsonata` dependency. Install optional dependencies or add `jsonata` to use `--bom-audit`.",
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Helper: Extract property value from CycloneDX properties array
|
|
26
|
+
* Usage in JSONata: $prop(component, 'cdx:github:action:isShaPinned')
|
|
27
|
+
* Returns string value or null if not found
|
|
28
|
+
*/
|
|
29
|
+
function extractProperty(obj, propName) {
|
|
30
|
+
if (!obj?.properties || !Array.isArray(obj.properties)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const prop = obj.properties.find((p) => p?.name === propName);
|
|
34
|
+
return prop?.value ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Helper: Check if property exists and equals expected value
|
|
39
|
+
* Usage: $hasProp(component, 'cdx:foo', 'bar')
|
|
40
|
+
*/
|
|
41
|
+
function hasProperty(obj, propName, expectedValue) {
|
|
42
|
+
const value = extractProperty(obj, propName);
|
|
43
|
+
if (expectedValue === undefined) {
|
|
44
|
+
return value !== null;
|
|
45
|
+
}
|
|
46
|
+
return value === String(expectedValue);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Helper: Safe JSONata evaluation with timeout protection
|
|
51
|
+
*/
|
|
52
|
+
async function safeEvaluate(expression, context, timeoutMs = 5000) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
reject(new Error(`JSONata evaluation timeout after ${timeoutMs}ms`));
|
|
56
|
+
}, timeoutMs);
|
|
57
|
+
|
|
58
|
+
expression
|
|
59
|
+
.evaluate(context)
|
|
60
|
+
.then((result) => {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
resolve(result);
|
|
63
|
+
})
|
|
64
|
+
.catch((err) => {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
reject(err);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Register custom JSONata functions for CycloneDX property access
|
|
73
|
+
*/
|
|
74
|
+
function registerCdxHelpers(expression) {
|
|
75
|
+
expression.registerFunction("prop", (obj, propName) =>
|
|
76
|
+
extractProperty(obj, propName),
|
|
77
|
+
);
|
|
78
|
+
expression.registerFunction("nullSafeProp", (obj, propName) => {
|
|
79
|
+
const value = extractProperty(obj, propName);
|
|
80
|
+
return value === null ? "" : value;
|
|
81
|
+
});
|
|
82
|
+
expression.registerFunction("hasProp", (obj, propName, expectedValue) =>
|
|
83
|
+
hasProperty(obj, propName, expectedValue),
|
|
84
|
+
);
|
|
85
|
+
expression.registerFunction("p", (obj, propName) =>
|
|
86
|
+
extractProperty(obj, propName),
|
|
87
|
+
);
|
|
88
|
+
expression.registerFunction("hasP", (obj, propName, expectedValue) =>
|
|
89
|
+
hasProperty(obj, propName, expectedValue),
|
|
90
|
+
);
|
|
91
|
+
expression.registerFunction("startsWith", (str, prefix) => {
|
|
92
|
+
if (typeof str !== "string" || typeof prefix !== "string") {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return str.startsWith(prefix);
|
|
96
|
+
});
|
|
97
|
+
expression.registerFunction("endsWith", (str, suffix) => {
|
|
98
|
+
if (typeof str !== "string" || typeof suffix !== "string") {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return str.endsWith(suffix);
|
|
102
|
+
});
|
|
103
|
+
expression.registerFunction("arrayContains", (arr, value) => {
|
|
104
|
+
if (!Array.isArray(arr)) return false;
|
|
105
|
+
return arr.includes(value);
|
|
106
|
+
});
|
|
107
|
+
expression.registerFunction("propBool", (obj, propName) => {
|
|
108
|
+
const val = extractProperty(obj, propName);
|
|
109
|
+
if (val === null || val === undefined) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (typeof val === "boolean") {
|
|
113
|
+
return val;
|
|
114
|
+
}
|
|
115
|
+
if (typeof val === "string") {
|
|
116
|
+
const normalized = val.trim().toLowerCase();
|
|
117
|
+
if (normalized === "true") {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (normalized === "false") {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
});
|
|
126
|
+
expression.registerFunction("propList", (obj, propName) => {
|
|
127
|
+
const val = extractProperty(obj, propName);
|
|
128
|
+
if (!val || typeof val !== "string") return [];
|
|
129
|
+
return val
|
|
130
|
+
.split(",")
|
|
131
|
+
.map((s) => s.trim())
|
|
132
|
+
.filter((s) => s.length > 0);
|
|
133
|
+
});
|
|
134
|
+
expression.registerFunction("listContains", (val, target) => {
|
|
135
|
+
if (Array.isArray(val)) {
|
|
136
|
+
return val.some((item) => String(item).trim() === String(target).trim());
|
|
137
|
+
}
|
|
138
|
+
if (typeof val === "string") {
|
|
139
|
+
return val
|
|
140
|
+
.split(",")
|
|
141
|
+
.some((item) => item.trim() === String(target).trim());
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
});
|
|
145
|
+
expression.registerFunction("safeStr", (val) => {
|
|
146
|
+
return val === null || val === undefined ? "" : String(val).trim();
|
|
147
|
+
});
|
|
148
|
+
return expression;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Load and validate rules from a directory of YAML files
|
|
153
|
+
* @param {string} rulesDir - Path to directory containing .yaml rule files
|
|
154
|
+
* @returns {Promise<Array>} Array of parsed rule objects
|
|
155
|
+
*/
|
|
156
|
+
export async function loadRules(rulesDir) {
|
|
157
|
+
const rules = [];
|
|
158
|
+
if (!safeExistsSync(rulesDir)) {
|
|
159
|
+
if (DEBUG_MODE) {
|
|
160
|
+
console.warn(`Rules directory not found: ${rulesDir}`);
|
|
161
|
+
}
|
|
162
|
+
return rules;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
if (!statSync(rulesDir)?.isDirectory()) {
|
|
166
|
+
if (DEBUG_MODE) {
|
|
167
|
+
console.warn(`Rules path is not a directory: ${rulesDir}`);
|
|
168
|
+
}
|
|
169
|
+
return rules;
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (DEBUG_MODE) {
|
|
173
|
+
console.warn(`Cannot stat rules directory ${rulesDir}:`, err.message);
|
|
174
|
+
}
|
|
175
|
+
return rules;
|
|
176
|
+
}
|
|
177
|
+
let ruleFiles = [];
|
|
178
|
+
try {
|
|
179
|
+
ruleFiles = readdirSync(rulesDir);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (DEBUG_MODE) {
|
|
182
|
+
console.warn(`Cannot read rules directory ${rulesDir}:`, err.message);
|
|
183
|
+
}
|
|
184
|
+
return rules;
|
|
185
|
+
}
|
|
186
|
+
for (const file of ruleFiles) {
|
|
187
|
+
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const filePath = join(rulesDir, file);
|
|
191
|
+
try {
|
|
192
|
+
if (!statSync(filePath).isFile()) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
} catch (_err) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const content = loadYaml(readFileSync(filePath, "utf-8"));
|
|
200
|
+
const fileRules = Array.isArray(content) ? content : [content];
|
|
201
|
+
for (const rule of fileRules) {
|
|
202
|
+
if (!rule.id || typeof rule.id !== "string") {
|
|
203
|
+
console.warn(`Rule in ${file} missing required field: id (string)`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!rule.condition || typeof rule.condition !== "string") {
|
|
207
|
+
console.warn(
|
|
208
|
+
`Rule ${rule.id} missing required field: condition (string)`,
|
|
209
|
+
);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (!rule.message || typeof rule.message !== "string") {
|
|
213
|
+
console.warn(
|
|
214
|
+
`Rule ${rule.id} missing required field: message (string)`,
|
|
215
|
+
);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
rule.severity = rule.severity || "medium";
|
|
219
|
+
rule.category = rule.category || "unknown";
|
|
220
|
+
if (!["critical", "high", "medium", "low"].includes(rule.severity)) {
|
|
221
|
+
console.warn(
|
|
222
|
+
`Rule ${rule.id} has invalid severity '${rule.severity}'; defaulting to 'medium'`,
|
|
223
|
+
);
|
|
224
|
+
rule.severity = "medium";
|
|
225
|
+
}
|
|
226
|
+
rules.push(rule);
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.warn(`Failed to load rule file ${filePath}:`, err.message);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (DEBUG_MODE) {
|
|
233
|
+
console.log(`Loaded ${rules.length} audit rules from ${rulesDir}`);
|
|
234
|
+
}
|
|
235
|
+
return rules;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Interpolate template strings with JSONata expressions
|
|
240
|
+
* Supports {{ expression }} syntax for dynamic message/evidence generation
|
|
241
|
+
*/
|
|
242
|
+
async function interpolateTemplate(template, context) {
|
|
243
|
+
if (!template || typeof template !== "string") {
|
|
244
|
+
return template;
|
|
245
|
+
}
|
|
246
|
+
const templateRegex = /\{\{\s*([^}]+)\s*}}/g;
|
|
247
|
+
let result = template;
|
|
248
|
+
const matches = [...template.matchAll(templateRegex)];
|
|
249
|
+
for (const match of matches) {
|
|
250
|
+
const [fullMatch, expr] = match;
|
|
251
|
+
try {
|
|
252
|
+
const expression = jsonata(expr.trim());
|
|
253
|
+
registerCdxHelpers(expression);
|
|
254
|
+
const value = await safeEvaluate(expression, context);
|
|
255
|
+
const replacement = value !== undefined ? String(value) : fullMatch;
|
|
256
|
+
result = result.replace(fullMatch, replacement);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (DEBUG_MODE) {
|
|
259
|
+
console.warn(
|
|
260
|
+
`Template interpolation failed for '{{${expr}}}':`,
|
|
261
|
+
err.message,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Evaluate a single rule against the BOM using JSONata
|
|
271
|
+
* @param {Object} rule - Parsed rule object
|
|
272
|
+
* @param {Object} bomJson - Full CycloneDX BOM object
|
|
273
|
+
* @returns {Promise<Array>} Array of matched findings
|
|
274
|
+
*/
|
|
275
|
+
export async function evaluateRule(rule, bomJson) {
|
|
276
|
+
const findings = [];
|
|
277
|
+
try {
|
|
278
|
+
const conditionExpr = jsonata(rule.condition);
|
|
279
|
+
registerCdxHelpers(conditionExpr);
|
|
280
|
+
const conditionResult = await safeEvaluate(conditionExpr, bomJson);
|
|
281
|
+
const matches = Array.isArray(conditionResult)
|
|
282
|
+
? conditionResult.filter((m) => m !== null && m !== undefined)
|
|
283
|
+
: conditionResult
|
|
284
|
+
? [conditionResult]
|
|
285
|
+
: [];
|
|
286
|
+
if (matches.length === 0) {
|
|
287
|
+
return findings;
|
|
288
|
+
}
|
|
289
|
+
for (const item of matches) {
|
|
290
|
+
const context = {
|
|
291
|
+
...item,
|
|
292
|
+
bom: bomJson,
|
|
293
|
+
components: bomJson.components || [],
|
|
294
|
+
workflows: bomJson.formulation?.[0]?.workflows || [],
|
|
295
|
+
services: bomJson.services || [],
|
|
296
|
+
metadata: bomJson.metadata || {},
|
|
297
|
+
};
|
|
298
|
+
const message = await interpolateTemplate(rule.message, context);
|
|
299
|
+
let location = null;
|
|
300
|
+
if (rule.location) {
|
|
301
|
+
try {
|
|
302
|
+
const locationExpr = jsonata(rule.location);
|
|
303
|
+
registerCdxHelpers(locationExpr);
|
|
304
|
+
location = await safeEvaluate(locationExpr, context);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (DEBUG_MODE) {
|
|
307
|
+
console.warn(
|
|
308
|
+
`Failed to extract location for rule ${rule.id}:`,
|
|
309
|
+
err.message,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
let evidence = null;
|
|
315
|
+
if (rule.evidence) {
|
|
316
|
+
try {
|
|
317
|
+
const evidenceExpr = jsonata(rule.evidence);
|
|
318
|
+
registerCdxHelpers(evidenceExpr);
|
|
319
|
+
evidence = await safeEvaluate(evidenceExpr, context);
|
|
320
|
+
} catch (err) {
|
|
321
|
+
if (DEBUG_MODE) {
|
|
322
|
+
console.warn(
|
|
323
|
+
`Failed to extract evidence for rule ${rule.id}:`,
|
|
324
|
+
err.message,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
findings.push({
|
|
330
|
+
ruleId: rule.id,
|
|
331
|
+
name: rule.name || rule.id,
|
|
332
|
+
description: rule.description,
|
|
333
|
+
severity: rule.severity,
|
|
334
|
+
category: rule.category,
|
|
335
|
+
message,
|
|
336
|
+
mitigation: rule.mitigation,
|
|
337
|
+
location,
|
|
338
|
+
evidence,
|
|
339
|
+
_match: item,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.warn(
|
|
344
|
+
`Failed to evaluate rule ${rule?.id || "unknown"}:`,
|
|
345
|
+
err.message,
|
|
346
|
+
);
|
|
347
|
+
if (DEBUG_MODE && err.stack) {
|
|
348
|
+
console.debug(err.stack);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return findings;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Evaluate all rules against a BOM
|
|
356
|
+
*/
|
|
357
|
+
export async function evaluateRules(rules, bomJson) {
|
|
358
|
+
const allFindings = [];
|
|
359
|
+
for (const rule of rules) {
|
|
360
|
+
const findings = await evaluateRule(rule, bomJson);
|
|
361
|
+
allFindings.push(...findings);
|
|
362
|
+
}
|
|
363
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
364
|
+
allFindings.sort((a, b) => {
|
|
365
|
+
const sevDiff = severityOrder[a.severity] - severityOrder[b.severity];
|
|
366
|
+
return sevDiff !== 0 ? sevDiff : a.ruleId.localeCompare(b.ruleId);
|
|
367
|
+
});
|
|
368
|
+
return allFindings;
|
|
369
|
+
}
|