fullshield 0.4.0 → 1.0.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/CHANGELOG.md +62 -0
- package/README.md +15 -3
- package/dist/agent.js +50 -10582
- package/dist/bench/score.js +2 -118
- package/dist/cli/index.js +134 -15538
- package/dist/compliance.js +2 -164
- package/dist/confirm.js +2 -146
- package/dist/dashboard.js +2 -179
- package/dist/iac.js +2 -132
- package/dist/incremental.js +2 -13
- package/dist/index.d.ts +191 -5
- package/dist/index.js +94 -11869
- package/dist/llm.js +2 -105
- package/dist/mcp.js +48 -10692
- package/dist/runtime/auto.js +5 -1260
- package/dist/runtime/dlp.js +1 -313
- package/dist/runtime/express.js +1 -579
- package/dist/runtime/fastify.js +1 -579
- package/dist/runtime/hono.js +1 -570
- package/dist/runtime/index.js +1 -229
- package/dist/runtime/koa.js +1 -573
- package/dist/runtime/rasp/loader.js +1 -14
- package/dist/runtime/rasp.js +14 -1436
- package/dist/runtime/register.js +6 -1268
- package/dist/supply.js +8 -324
- package/dist/threat.js +3 -63
- package/dist/vex.js +2 -180
- package/dist/workspaces.js +2 -23
- package/models/index.json +356 -0
- package/package.json +22 -7
- package/rules/DEPS-LOCKMISMATCH-001.md +7 -0
- package/rules/DEPS-PHANTOM-001.md +7 -0
- package/rules/README.md +1 -0
- package/rules/SECRET-AWS-001.md +6 -0
- package/rules/SECRET-GITHUB-001.md +6 -0
- package/rules/SUPPLY-CAP-001.md +17 -0
- package/rules/SUPPLY-PROV-001.md +15 -0
package/dist/supply.js
CHANGED
|
@@ -1,324 +1,8 @@
|
|
|
1
|
-
import { existsSync, statSync, readFileSync,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// src/supply/experimental.ts
|
|
12
|
-
function experimentalVex() {
|
|
13
|
-
return process.env.FULLSHIELD_EXPERIMENTAL_VEX === "1";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// src/supply/index.ts
|
|
17
|
-
var REGISTRY_HOST = "registry.npmjs.org";
|
|
18
|
-
var INSTALL_SCRIPT_KEYS = ["preinstall", "install", "postinstall"];
|
|
19
|
-
function assertRegistryUrl(url) {
|
|
20
|
-
let parsed;
|
|
21
|
-
try {
|
|
22
|
-
parsed = new URL(url);
|
|
23
|
-
} catch {
|
|
24
|
-
throw new Error("supply: registry URL must be https://registry.npmjs.org/...");
|
|
25
|
-
}
|
|
26
|
-
if (parsed.protocol !== "https:" || parsed.hostname !== REGISTRY_HOST) {
|
|
27
|
-
throw new Error(`supply: only ${REGISTRY_HOST} is allowed (got ${parsed.hostname || url})`);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function packageNameFromSpec(spec) {
|
|
31
|
-
if (spec.startsWith("@")) {
|
|
32
|
-
const slash = spec.indexOf("/");
|
|
33
|
-
if (slash < 0) return spec;
|
|
34
|
-
const scope = spec.slice(0, slash);
|
|
35
|
-
const rest = spec.slice(slash + 1);
|
|
36
|
-
const name = rest.includes("@") ? rest.slice(0, rest.indexOf("@")) : rest;
|
|
37
|
-
return `${scope}/${name}`;
|
|
38
|
-
}
|
|
39
|
-
const at = spec.indexOf("@");
|
|
40
|
-
return at > 0 ? spec.slice(0, at) : spec;
|
|
41
|
-
}
|
|
42
|
-
function finding(ruleId, title, message, file, severity = "high") {
|
|
43
|
-
return {
|
|
44
|
-
id: makeFindingId(ruleId, file, message),
|
|
45
|
-
ruleId,
|
|
46
|
-
module: "deps",
|
|
47
|
-
title,
|
|
48
|
-
severity,
|
|
49
|
-
confidence: "high",
|
|
50
|
-
file,
|
|
51
|
-
message,
|
|
52
|
-
remediation: "Do not install this package until the behavior is reviewed. Use --ignore-scripts."
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
function walkJs(dir, out = []) {
|
|
56
|
-
if (!existsSync(dir)) return out;
|
|
57
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
58
|
-
const full = join(dir, entry.name);
|
|
59
|
-
if (entry.isDirectory()) {
|
|
60
|
-
if (entry.name === "node_modules") continue;
|
|
61
|
-
walkJs(full, out);
|
|
62
|
-
} else if (entry.isFile() && /\.(js|cjs|mjs|ts)$/.test(entry.name)) {
|
|
63
|
-
out.push(full);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
function readJson(path) {
|
|
69
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
70
|
-
}
|
|
71
|
-
function installScriptKeys(scripts) {
|
|
72
|
-
if (!scripts) return [];
|
|
73
|
-
return INSTALL_SCRIPT_KEYS.filter((k) => typeof scripts[k] === "string");
|
|
74
|
-
}
|
|
75
|
-
function analyzePackageDir(dir) {
|
|
76
|
-
const findings = [];
|
|
77
|
-
const manifestPath = join(dir, "package.json");
|
|
78
|
-
if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) return findings;
|
|
79
|
-
const pkg = readJson(manifestPath);
|
|
80
|
-
const scripts = pkg.scripts ?? {};
|
|
81
|
-
const keys = installScriptKeys(scripts);
|
|
82
|
-
if (keys.length > 0) {
|
|
83
|
-
findings.push(
|
|
84
|
-
finding(
|
|
85
|
-
"SUPPLY-INSTALLSCRIPT-001",
|
|
86
|
-
"Package declares an install script",
|
|
87
|
-
`install scripts: ${keys.join(", ")}`,
|
|
88
|
-
"package.json"
|
|
89
|
-
)
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
const sources = walkJs(dir).map((file) => ({
|
|
93
|
-
file: relative(dir, file) || file,
|
|
94
|
-
text: readFileSync(file, "utf8")
|
|
95
|
-
}));
|
|
96
|
-
const readsNpmrc = sources.some((s) => s.text.includes(".npmrc") || s.text.includes(".ssh"));
|
|
97
|
-
const hasNet = sources.some((s) => /https\.request|http\.request|fetch\s*\(|axios\./.test(s.text));
|
|
98
|
-
if (readsNpmrc && hasNet) {
|
|
99
|
-
const hit = sources.find((s) => s.text.includes(".npmrc") || s.text.includes(".ssh"));
|
|
100
|
-
findings.push(
|
|
101
|
-
finding(
|
|
102
|
-
"SUPPLY-NPMRC-NET-001",
|
|
103
|
-
"Install-time code reads npm/ssh credentials and uses the network",
|
|
104
|
-
"static: credential file read combined with http(s)/fetch",
|
|
105
|
-
hit?.file,
|
|
106
|
-
"critical"
|
|
107
|
-
)
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
for (const src of sources) {
|
|
111
|
-
if (/eval\s*\(\s*(Buffer|atob|fromCharCode|decodeURIComponent)/.test(src.text)) {
|
|
112
|
-
findings.push(
|
|
113
|
-
finding(
|
|
114
|
-
"SUPPLY-OBFUSCATE-001",
|
|
115
|
-
"Package evals a decoded/obfuscated string",
|
|
116
|
-
"static: eval of decoded payload",
|
|
117
|
-
src.file
|
|
118
|
-
)
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (experimentalVex()) {
|
|
123
|
-
findings.push(...analyzeWormIndicators(dir));
|
|
124
|
-
findings.push(...analyzeWorkflowTampering(dir));
|
|
125
|
-
}
|
|
126
|
-
return findings;
|
|
127
|
-
}
|
|
128
|
-
function diffPackageVersions(oldDir, newDir) {
|
|
129
|
-
const oldManifest = existsSync(join(oldDir, "package.json")) ? readJson(join(oldDir, "package.json")) : {};
|
|
130
|
-
const nextManifest = existsSync(join(newDir, "package.json")) ? readJson(join(newDir, "package.json")) : {};
|
|
131
|
-
const oldScripts = oldManifest.scripts ?? {};
|
|
132
|
-
const nextScripts = nextManifest.scripts ?? {};
|
|
133
|
-
const added = INSTALL_SCRIPT_KEYS.filter((k) => typeof nextScripts[k] === "string" && oldScripts[k] === void 0);
|
|
134
|
-
const oldSrc = walkJs(oldDir).map((f) => readFileSync(f, "utf8")).join("\n");
|
|
135
|
-
const newSrc = walkJs(newDir).map((f) => readFileSync(f, "utf8")).join("\n");
|
|
136
|
-
const newChild = /child_process/.test(newSrc) && !/child_process/.test(oldSrc);
|
|
137
|
-
const newNet = /https?\.request|fetch\s*\(/.test(newSrc) && !/https?\.request|fetch\s*\(/.test(oldSrc);
|
|
138
|
-
if (added.length === 0 && !newChild && !newNet) return [];
|
|
139
|
-
return [
|
|
140
|
-
finding(
|
|
141
|
-
"SUPPLY-BEHAVIOR-001",
|
|
142
|
-
"Package version adds install-script or process/network behavior",
|
|
143
|
-
`added scripts=${added.join(",") || "none"} child_process=${newChild} network=${newNet}`,
|
|
144
|
-
"package.json"
|
|
145
|
-
)
|
|
146
|
-
];
|
|
147
|
-
}
|
|
148
|
-
function checkCooldown(packument, version, minHours = 72, nowMs = Date.now()) {
|
|
149
|
-
const stamp = packument.time?.[version];
|
|
150
|
-
if (!stamp) return [];
|
|
151
|
-
const published = Date.parse(stamp);
|
|
152
|
-
if (Number.isNaN(published)) return [];
|
|
153
|
-
const ageHours = (nowMs - published) / 36e5;
|
|
154
|
-
if (ageHours >= minHours) return [];
|
|
155
|
-
return [
|
|
156
|
-
finding(
|
|
157
|
-
"SUPPLY-COOLDOWN-001",
|
|
158
|
-
"Package version is younger than the release cooldown",
|
|
159
|
-
`${version} published ${ageHours.toFixed(1)}h ago (min ${minHours}h)`,
|
|
160
|
-
void 0,
|
|
161
|
-
"medium"
|
|
162
|
-
)
|
|
163
|
-
];
|
|
164
|
-
}
|
|
165
|
-
function checkLockfileIntegrity(lock) {
|
|
166
|
-
const findings = [];
|
|
167
|
-
for (const [name, entry] of Object.entries(lock.packages ?? {})) {
|
|
168
|
-
if (name === "") continue;
|
|
169
|
-
const resolved = entry.resolved;
|
|
170
|
-
if (!resolved) continue;
|
|
171
|
-
if (entry.integrity === void 0 || entry.integrity.length === 0) {
|
|
172
|
-
findings.push(
|
|
173
|
-
finding(
|
|
174
|
-
"SUPPLY-LOCKINTEGRITY-001",
|
|
175
|
-
"Lockfile entry has no integrity hash",
|
|
176
|
-
`${name} resolved without an integrity hash`,
|
|
177
|
-
"package-lock.json"
|
|
178
|
-
)
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
try {
|
|
182
|
-
const url = new URL(resolved);
|
|
183
|
-
if (url.protocol !== "https:" || url.hostname !== REGISTRY_HOST) {
|
|
184
|
-
findings.push(
|
|
185
|
-
finding(
|
|
186
|
-
"SUPPLY-LOCKURL-001",
|
|
187
|
-
"Lockfile entry resolves from a non-npm registry host",
|
|
188
|
-
`${name} resolved host ${url.hostname || resolved}`,
|
|
189
|
-
"package-lock.json"
|
|
190
|
-
)
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
} catch {
|
|
194
|
-
findings.push(
|
|
195
|
-
finding(
|
|
196
|
-
"SUPPLY-LOCKURL-001",
|
|
197
|
-
"Lockfile entry has a non-URL resolved field",
|
|
198
|
-
`${name} resolved ${resolved}`,
|
|
199
|
-
"package-lock.json"
|
|
200
|
-
)
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return findings;
|
|
205
|
-
}
|
|
206
|
-
function checkDependencyConfusion(pkgName, npmrc, publicExists) {
|
|
207
|
-
const scoped = pkgName.startsWith("@");
|
|
208
|
-
const privateRegistry = /@[\w-]+\s*:\s*registry\s*=/.test(npmrc) || /registry\s*=\s*https?:\/\/(?!registry\.npmjs\.org)/.test(npmrc);
|
|
209
|
-
if (scoped && privateRegistry && publicExists) {
|
|
210
|
-
return [
|
|
211
|
-
finding(
|
|
212
|
-
"SUPPLY-CONFUSION-001",
|
|
213
|
-
"Private-scope name also exists on the public registry",
|
|
214
|
-
`${pkgName} is configured for a private registry but a public package exists`
|
|
215
|
-
)
|
|
216
|
-
];
|
|
217
|
-
}
|
|
218
|
-
return [];
|
|
219
|
-
}
|
|
220
|
-
function checkProvenance(previouslyHad, currentlyHas) {
|
|
221
|
-
if (previouslyHad && !currentlyHas) {
|
|
222
|
-
return [
|
|
223
|
-
finding(
|
|
224
|
-
"SUPPLY-PROVENANCE-001",
|
|
225
|
-
"Package dropped npm provenance since the last scan",
|
|
226
|
-
"previous version had provenance; current version does not"
|
|
227
|
-
)
|
|
228
|
-
];
|
|
229
|
-
}
|
|
230
|
-
return [];
|
|
231
|
-
}
|
|
232
|
-
async function installGuard(spec, opts = {}) {
|
|
233
|
-
if (!opts.experimental && process.env.FULLSHIELD_EXPERIMENTAL_SUPPLY !== "1") {
|
|
234
|
-
return { findings: [], skipped: true };
|
|
235
|
-
}
|
|
236
|
-
const name = packageNameFromSpec(spec);
|
|
237
|
-
const url = `https://${REGISTRY_HOST}/${encodeURIComponent(name)}`;
|
|
238
|
-
assertRegistryUrl(url);
|
|
239
|
-
if (opts.fetchPackument) await opts.fetchPackument(url);
|
|
240
|
-
const findings = opts.extractDir !== void 0 ? analyzePackageDir(opts.extractDir) : [];
|
|
241
|
-
if (findings.length === 0 && opts.runNpm) {
|
|
242
|
-
await opts.runNpm(["install", spec, "--ignore-scripts"]);
|
|
243
|
-
}
|
|
244
|
-
return { findings };
|
|
245
|
-
}
|
|
246
|
-
var TOKEN_RE = /(?:~\/)?\.npmrc\b|\bNPM_TOKEN\b|\bGITHUB_TOKEN\b/;
|
|
247
|
-
var PUBLISH_RE = /\bnpm\s+publish\b|\bnpmPublish\b|\blibnpmpublish\b/;
|
|
248
|
-
var REGISTRY_PUT_RE = /registry\.npmjs\.org[\s\S]{0,800}\bPUT\b|\bmethod\s*:\s*['"]PUT['"][\s\S]{0,400}registry\.npmjs\.org/;
|
|
249
|
-
var GITHUB_CREATE_RE = /api\.github\.com\/(?:user\/repos|repos\/)|\/actions\/workflows/;
|
|
250
|
-
var WORKFLOW_PATH_RE = /\.github\/workflows/;
|
|
251
|
-
var CI_WRITE_RE = /\.github\/|\.git\/hooks\/|\.gitlab-ci\.yml|\.travis\.yml|azure-pipelines(?:\.yml)?|Jenkinsfile|\.circleci\//;
|
|
252
|
-
function packageSources(dir) {
|
|
253
|
-
const manifestPath = join(dir, "package.json");
|
|
254
|
-
let scripts = "";
|
|
255
|
-
if (existsSync(manifestPath) && statSync(manifestPath).isFile()) {
|
|
256
|
-
try {
|
|
257
|
-
const pkg = readJson(manifestPath);
|
|
258
|
-
const block = pkg.scripts ?? {};
|
|
259
|
-
scripts = Object.values(block).join("\n");
|
|
260
|
-
} catch {
|
|
261
|
-
scripts = "";
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
const sources = walkJs(dir).map((file) => ({
|
|
265
|
-
file: relative(dir, file) || file,
|
|
266
|
-
text: readFileSync(file, "utf8")
|
|
267
|
-
}));
|
|
268
|
-
return { sources, scripts };
|
|
269
|
-
}
|
|
270
|
-
function analyzeWormIndicators(dir) {
|
|
271
|
-
const { sources, scripts } = packageSources(dir);
|
|
272
|
-
if (sources.length === 0 && scripts.length === 0) return [];
|
|
273
|
-
const text = `${sources.map((s) => s.text).join("\n")}
|
|
274
|
-
${scripts}`;
|
|
275
|
-
if (!TOKEN_RE.test(text)) return [];
|
|
276
|
-
const replicates = PUBLISH_RE.test(text) || REGISTRY_PUT_RE.test(text) || GITHUB_CREATE_RE.test(text) || WORKFLOW_PATH_RE.test(text);
|
|
277
|
-
if (!replicates) return [];
|
|
278
|
-
const hit = sources.find(
|
|
279
|
-
(s) => TOKEN_RE.test(s.text) && (PUBLISH_RE.test(s.text) || REGISTRY_PUT_RE.test(s.text) || GITHUB_CREATE_RE.test(s.text) || WORKFLOW_PATH_RE.test(s.text))
|
|
280
|
-
) ?? sources.find((s) => TOKEN_RE.test(s.text));
|
|
281
|
-
return [
|
|
282
|
-
finding(
|
|
283
|
-
"SUPPLY-WORM-001",
|
|
284
|
-
"Self-replicating worm indicators in dependency code",
|
|
285
|
-
"static: npm/GitHub token read combined with publish, registry PUT, GitHub repo/workflow creation, or writing .github/workflows",
|
|
286
|
-
hit?.file,
|
|
287
|
-
"critical"
|
|
288
|
-
)
|
|
289
|
-
];
|
|
290
|
-
}
|
|
291
|
-
function analyzeWorkflowTampering(dir) {
|
|
292
|
-
const { sources, scripts } = packageSources(dir);
|
|
293
|
-
const text = `${sources.map((s) => s.text).join("\n")}
|
|
294
|
-
${scripts}`;
|
|
295
|
-
if (!CI_WRITE_RE.test(text)) return [];
|
|
296
|
-
const hit = sources.find((s) => CI_WRITE_RE.test(s.text));
|
|
297
|
-
return [
|
|
298
|
-
finding(
|
|
299
|
-
"SUPPLY-WORKFLOW-001",
|
|
300
|
-
"Dependency writes GitHub workflow, git hooks, or CI config",
|
|
301
|
-
"static: install script or package code writes .github/, .git/hooks/, or CI configuration",
|
|
302
|
-
hit?.file ?? (CI_WRITE_RE.test(scripts) ? "package.json" : void 0),
|
|
303
|
-
"critical"
|
|
304
|
-
)
|
|
305
|
-
];
|
|
306
|
-
}
|
|
307
|
-
function provenanceCoverage(packages, known) {
|
|
308
|
-
let withProvenance = 0;
|
|
309
|
-
let unknown = 0;
|
|
310
|
-
for (const pkg of packages) {
|
|
311
|
-
const hit = known.get(`${pkg.name}@${pkg.version}`);
|
|
312
|
-
if (hit === true) withProvenance += 1;
|
|
313
|
-
else if (hit === void 0) unknown += 1;
|
|
314
|
-
}
|
|
315
|
-
const total = packages.length;
|
|
316
|
-
return {
|
|
317
|
-
total,
|
|
318
|
-
withProvenance,
|
|
319
|
-
unknown,
|
|
320
|
-
percent: total === 0 ? 0 : withProvenance / total * 100
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export { analyzePackageDir, analyzeWorkflowTampering, analyzeWormIndicators, assertRegistryUrl, checkCooldown, checkDependencyConfusion, checkLockfileIntegrity, checkProvenance, diffPackageVersions, installGuard, packageNameFromSpec, provenanceCoverage };
|
|
1
|
+
import {existsSync,statSync,readFileSync,readdirSync}from'fs';import {join,relative}from'path';import {createHash}from'crypto';function S(t,e,n){let i=(n??"").replace(/\s+/g," ").trim();return createHash("sha256").update(`${t}\0${e??""}\0${i}`).digest("hex").slice(0,16)}function v(){return process.env.FULLSHIELD_EXPERIMENTAL_VEX==="1"}var y="registry.npmjs.org",I=["preinstall","install","postinstall"];function j(t){let e;try{e=new URL(t);}catch{throw new Error("supply: registry URL must be https://registry.npmjs.org/...")}if(e.protocol!=="https:"||e.hostname!==y)throw new Error(`supply: only ${y} is allowed (got ${e.hostname||t})`)}function C(t){if(t.startsWith("@")){let n=t.indexOf("/");if(n<0)return t;let i=t.slice(0,n),r=t.slice(n+1),s=r.includes("@")?r.slice(0,r.indexOf("@")):r;return `${i}/${s}`}let e=t.indexOf("@");return e>0?t.slice(0,e):t}function a(t,e,n,i,r="high"){return {id:S(t,i,n),ruleId:t,module:"deps",title:e,severity:r,confidence:"high",file:i,message:n,remediation:"Do not install this package until the behavior is reviewed. Use --ignore-scripts."}}function f(t,e=[]){if(!existsSync(t))return e;for(let n of readdirSync(t,{withFileTypes:true})){let i=join(t,n.name);if(n.isDirectory()){if(n.name==="node_modules")continue;f(i,e);}else n.isFile()&&/\.(js|cjs|mjs|ts)$/.test(n.name)&&e.push(i);}return e}function x(t){return JSON.parse(readFileSync(t,"utf8"))}function T(t){return t?I.filter(e=>typeof t[e]=="string"):[]}function $(t){let e=[],n=join(t,"package.json");if(!existsSync(n)||!statSync(n).isFile())return e;let r=x(n).scripts??{},s=T(r);s.length>0&&e.push(a("SUPPLY-INSTALLSCRIPT-001","Package declares an install script",`install scripts: ${s.join(", ")}`,"package.json"));let o=f(t).map(c=>({file:relative(t,c)||c,text:readFileSync(c,"utf8")})),m=o.some(c=>c.text.includes(".npmrc")||c.text.includes(".ssh")),h=o.some(c=>/https\.request|http\.request|fetch\s*\(|axios\./.test(c.text));if(m&&h){let c=o.find(d=>d.text.includes(".npmrc")||d.text.includes(".ssh"));e.push(a("SUPPLY-NPMRC-NET-001","Install-time code reads npm/ssh credentials and uses the network","static: credential file read combined with http(s)/fetch",c?.file,"critical"));}for(let c of o)/eval\s*\(\s*(Buffer|atob|fromCharCode|decodeURIComponent)/.test(c.text)&&e.push(a("SUPPLY-OBFUSCATE-001","Package evals a decoded/obfuscated string","static: eval of decoded payload",c.file));return v()&&(e.push(..._(t)),e.push(...A(t))),e}function B(t,e){let n=existsSync(join(t,"package.json"))?x(join(t,"package.json")):{},i=existsSync(join(e,"package.json"))?x(join(e,"package.json")):{},r=n.scripts??{},s=i.scripts??{},o=I.filter(u=>typeof s[u]=="string"&&r[u]===void 0),m=f(t).map(u=>readFileSync(u,"utf8")).join(`
|
|
2
|
+
`),h=f(e).map(u=>readFileSync(u,"utf8")).join(`
|
|
3
|
+
`),c=/child_process/.test(h)&&!/child_process/.test(m),d=/https?\.request|fetch\s*\(/.test(h)&&!/https?\.request|fetch\s*\(/.test(m);return o.length===0&&!c&&!d?[]:[a("SUPPLY-BEHAVIOR-001","Package version adds install-script or process/network behavior",`added scripts=${o.join(",")||"none"} child_process=${c} network=${d}`,"package.json")]}function V(t,e,n=72,i=Date.now()){let r=t.time?.[e];if(!r)return [];let s=Date.parse(r);if(Number.isNaN(s))return [];let o=(i-s)/36e5;return o>=n?[]:[a("SUPPLY-COOLDOWN-001","Package version is younger than the release cooldown",`${e} published ${o.toFixed(1)}h ago (min ${n}h)`,void 0,"medium")]}function z(t){let e=[];for(let[n,i]of Object.entries(t.packages??{})){if(n==="")continue;let r=i.resolved;if(r){(i.integrity===void 0||i.integrity.length===0)&&e.push(a("SUPPLY-LOCKINTEGRITY-001","Lockfile entry has no integrity hash",`${n} resolved without an integrity hash`,"package-lock.json"));try{let s=new URL(r);(s.protocol!=="https:"||s.hostname!==y)&&e.push(a("SUPPLY-LOCKURL-001","Lockfile entry resolves from a non-npm registry host",`${n} resolved host ${s.hostname||r}`,"package-lock.json"));}catch{e.push(a("SUPPLY-LOCKURL-001","Lockfile entry has a non-URL resolved field",`${n} resolved ${r}`,"package-lock.json"));}}}return e}function q(t,e,n){let i=t.startsWith("@"),r=/@[\w-]+\s*:\s*registry\s*=/.test(e)||/registry\s*=\s*https?:\/\/(?!registry\.npmjs\.org)/.test(e);return i&&r&&n?[a("SUPPLY-CONFUSION-001","Private-scope name also exists on the public registry",`${t} is configured for a private registry but a public package exists`)]:[]}function J(t,e){return t&&!e?[a("SUPPLY-PROVENANCE-001","Package dropped npm provenance since the last scan","previous version had provenance; current version does not")]:[]}async function X(t,e={}){if(!e.experimental&&process.env.FULLSHIELD_EXPERIMENTAL_SUPPLY!=="1")return {findings:[],skipped:true};let n=C(t),i=`https://${y}/${encodeURIComponent(n)}`;j(i),e.fetchPackument&&await e.fetchPackument(i);let r=e.extractDir!==void 0?$(e.extractDir):[];return r.length===0&&e.runNpm&&await e.runNpm(["install",t,"--ignore-scripts"]),{findings:r}}var b=/(?:~\/)?\.npmrc\b|\bNPM_TOKEN\b|\bGITHUB_TOKEN\b/,P=/\bnpm\s+publish\b|\bnpmPublish\b|\blibnpmpublish\b/,R=/registry\.npmjs\.org[\s\S]{0,800}\bPUT\b|\bmethod\s*:\s*['"]PUT['"][\s\S]{0,400}registry\.npmjs\.org/,w=/api\.github\.com\/(?:user\/repos|repos\/)|\/actions\/workflows/,E=/\.github\/workflows/,k=/\.github\/|\.git\/hooks\/|\.gitlab-ci\.yml|\.travis\.yml|azure-pipelines(?:\.yml)?|Jenkinsfile|\.circleci\//;function U(t){let e=join(t,"package.json"),n="";if(existsSync(e)&&statSync(e).isFile())try{let s=x(e).scripts??{};n=Object.values(s).join(`
|
|
4
|
+
`);}catch{n="";}return {sources:f(t).map(r=>({file:relative(t,r)||r,text:readFileSync(r,"utf8")})),scripts:n}}function _(t){let{sources:e,scripts:n}=U(t);if(e.length===0&&n.length===0)return [];let i=`${e.map(o=>o.text).join(`
|
|
5
|
+
`)}
|
|
6
|
+
${n}`;if(!b.test(i))return [];if(!(P.test(i)||R.test(i)||w.test(i)||E.test(i)))return [];let s=e.find(o=>b.test(o.text)&&(P.test(o.text)||R.test(o.text)||w.test(o.text)||E.test(o.text)))??e.find(o=>b.test(o.text));return [a("SUPPLY-WORM-001","Self-replicating worm indicators in dependency code","static: npm/GitHub token read combined with publish, registry PUT, GitHub repo/workflow creation, or writing .github/workflows",s?.file,"critical")]}function A(t){let{sources:e,scripts:n}=U(t),i=`${e.map(s=>s.text).join(`
|
|
7
|
+
`)}
|
|
8
|
+
${n}`;if(!k.test(i))return [];let r=e.find(s=>k.test(s.text));return [a("SUPPLY-WORKFLOW-001","Dependency writes GitHub workflow, git hooks, or CI config","static: install script or package code writes .github/, .git/hooks/, or CI configuration",r?.file??(k.test(n)?"package.json":void 0),"critical")]}function Q(t,e){let n=0,i=0;for(let s of t){let o=e.get(`${s.name}@${s.version}`);o===true?n+=1:o===void 0&&(i+=1);}let r=t.length;return {total:r,withProvenance:n,unknown:i,percent:r===0?0:n/r*100}}export{$ as analyzePackageDir,A as analyzeWorkflowTampering,_ as analyzeWormIndicators,j as assertRegistryUrl,V as checkCooldown,q as checkDependencyConfusion,z as checkLockfileIntegrity,J as checkProvenance,B as diffPackageVersions,X as installGuard,C as packageNameFromSpec,Q as provenanceCoverage};
|
package/dist/threat.js
CHANGED
|
@@ -1,63 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// src/threat/index.ts
|
|
5
|
-
function walk(dir, out = []) {
|
|
6
|
-
if (!existsSync(dir)) return out;
|
|
7
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
8
|
-
const full = join(dir, entry.name);
|
|
9
|
-
if (entry.isDirectory()) {
|
|
10
|
-
if (["node_modules", "dist", ".git"].includes(entry.name)) continue;
|
|
11
|
-
walk(full, out);
|
|
12
|
-
} else if (entry.isFile() && /\.[cm]?[jt]sx?$/.test(entry.name)) {
|
|
13
|
-
out.push(full);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
return out;
|
|
17
|
-
}
|
|
18
|
-
function buildThreatModel(dir, opts = {}) {
|
|
19
|
-
if (!opts.experimental && process.env.FULLSHIELD_EXPERIMENTAL_PLATFORM !== "1") {
|
|
20
|
-
return { skipped: true, routes: [], stores: [], egress: [], stride: [], mermaid: "" };
|
|
21
|
-
}
|
|
22
|
-
const routes = [];
|
|
23
|
-
const stores = /* @__PURE__ */ new Set();
|
|
24
|
-
const egress = /* @__PURE__ */ new Set();
|
|
25
|
-
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
26
|
-
return { routes, stores: [], egress: [], stride: [], mermaid: "flowchart LR\n" };
|
|
27
|
-
}
|
|
28
|
-
for (const file of walk(dir)) {
|
|
29
|
-
const rel = relative(dir, file) || file;
|
|
30
|
-
const text = readFileSync(file, "utf8");
|
|
31
|
-
const routeRe = /app\.(get|post|put|patch|delete|use)\(\s*['"]([^'"]+)['"]/g;
|
|
32
|
-
let m;
|
|
33
|
-
while ((m = routeRe.exec(text)) !== null) {
|
|
34
|
-
routes.push({
|
|
35
|
-
method: (m[1] ?? "get").toUpperCase(),
|
|
36
|
-
path: m[2] ?? "/",
|
|
37
|
-
file: rel,
|
|
38
|
-
auth: /auth|requireUser|requireAuth/.test(text)
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
if (/sqlite|mongodb|postgres|mysql|createPool|DatabaseSync/.test(text)) stores.add(rel);
|
|
42
|
-
const urlRe = /https?:\/\/[^\s'"[`]+/g;
|
|
43
|
-
while ((m = urlRe.exec(text)) !== null) egress.add(m[0] ?? "");
|
|
44
|
-
}
|
|
45
|
-
const stride = routes.slice(0, 20).map((r) => ({
|
|
46
|
-
element: `${r.method} ${r.path}`,
|
|
47
|
-
spoofing: r.auth ? "auth middleware present" : "no auth seen",
|
|
48
|
-
tampering: "input reaches handler",
|
|
49
|
-
repudiation: "check access logs",
|
|
50
|
-
infoDisclosure: "review response bodies",
|
|
51
|
-
denialOfService: "no rate-limit inferred",
|
|
52
|
-
elevation: r.auth ? "review authorization" : "unauthenticated route"
|
|
53
|
-
}));
|
|
54
|
-
const mermaid = [
|
|
55
|
-
"flowchart LR",
|
|
56
|
-
" client[Client] --> app[App]",
|
|
57
|
-
...routes.slice(0, 12).map((r, i) => ` app --> r${i}["${r.method} ${r.path}"]`),
|
|
58
|
-
...[...stores].slice(0, 6).map((s, i) => ` app --> s${i}["store ${s}"]`)
|
|
59
|
-
].join("\n");
|
|
60
|
-
return { routes, stores: [...stores], egress: [...egress], stride, mermaid };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export { buildThreatModel };
|
|
1
|
+
import {existsSync,statSync,readFileSync,readdirSync}from'fs';import {relative,join}from'path';function c(r,s=[]){if(!existsSync(r))return s;for(let t of readdirSync(r,{withFileTypes:true})){let i=join(r,t.name);if(t.isDirectory()){if(["node_modules","dist",".git"].includes(t.name))continue;c(i,s);}else t.isFile()&&/\.[cm]?[jt]sx?$/.test(t.name)&&s.push(i);}return s}function T(r,s={}){if(!s.experimental&&process.env.FULLSHIELD_EXPERIMENTAL_PLATFORM!=="1")return {skipped:true,routes:[],stores:[],egress:[],stride:[],mermaid:""};let t=[],i=new Set,l=new Set;if(!existsSync(r)||!statSync(r).isDirectory())return {routes:t,stores:[],egress:[],stride:[],mermaid:`flowchart LR
|
|
2
|
+
`};for(let e of c(r)){let n=relative(r,e)||e,a=readFileSync(e,"utf8"),g=/app\.(get|post|put|patch|delete|use)\(\s*['"]([^'"]+)['"]/g,o;for(;(o=g.exec(a))!==null;)t.push({method:(o[1]??"get").toUpperCase(),path:o[2]??"/",file:n,auth:/auth|requireUser|requireAuth/.test(a)});/sqlite|mongodb|postgres|mysql|createPool|DatabaseSync/.test(a)&&i.add(n);let h=/https?:\/\/[^\s'"[`]+/g;for(;(o=h.exec(a))!==null;)l.add(o[0]??"");}let u=t.slice(0,20).map(e=>({element:`${e.method} ${e.path}`,spoofing:e.auth?"auth middleware present":"no auth seen",tampering:"input reaches handler",repudiation:"check access logs",infoDisclosure:"review response bodies",denialOfService:"no rate-limit inferred",elevation:e.auth?"review authorization":"unauthenticated route"})),d=["flowchart LR"," client[Client] --> app[App]",...t.slice(0,12).map((e,n)=>` app --> r${n}["${e.method} ${e.path}"]`),...[...i].slice(0,6).map((e,n)=>` app --> s${n}["store ${e}"]`)].join(`
|
|
3
|
+
`);return {routes:t,stores:[...i],egress:[...l],stride:u,mermaid:d}}export{T as buildThreatModel};
|
package/dist/vex.js
CHANGED
|
@@ -1,180 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
// src/vex/index.ts
|
|
4
|
-
|
|
5
|
-
// src/sbom/cyclonedx.ts
|
|
6
|
-
function validateCycloneDx15(doc) {
|
|
7
|
-
const errors = [];
|
|
8
|
-
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
9
|
-
return ["CycloneDX document must be an object"];
|
|
10
|
-
}
|
|
11
|
-
const root = doc;
|
|
12
|
-
if (root.bomFormat !== "CycloneDX") errors.push('bomFormat must be "CycloneDX"');
|
|
13
|
-
if (root.specVersion !== "1.5") errors.push('specVersion must be "1.5"');
|
|
14
|
-
if (!Array.isArray(root.components)) errors.push("components must be an array");
|
|
15
|
-
return errors;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// src/vex/index.ts
|
|
19
|
-
var OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
|
|
20
|
-
var OPENVEX_NOT_AFFECTED_JUSTIFICATION = "vulnerable_code_not_in_execute_path";
|
|
21
|
-
var CDX_NOT_AFFECTED_JUSTIFICATION = "code_not_reachable";
|
|
22
|
-
var OPENVEX_STATUSES = /* @__PURE__ */ new Set(["not_affected", "affected", "fixed", "under_investigation"]);
|
|
23
|
-
function depFindings(findings) {
|
|
24
|
-
return findings.filter((f) => f.ruleId === "DEPS-VULN-001");
|
|
25
|
-
}
|
|
26
|
-
function vulnName(finding) {
|
|
27
|
-
return finding.references?.find((r) => r.startsWith("CVE-") || r.startsWith("GHSA-") || r.startsWith("MAL-")) ?? finding.id;
|
|
28
|
-
}
|
|
29
|
-
function npmPurl(snippet) {
|
|
30
|
-
const raw = snippet ?? "unknown@0.0.0";
|
|
31
|
-
let name;
|
|
32
|
-
let version;
|
|
33
|
-
if (raw.startsWith("@")) {
|
|
34
|
-
const second = raw.indexOf("@", 1);
|
|
35
|
-
name = second === -1 ? raw : raw.slice(0, second);
|
|
36
|
-
version = second === -1 ? "0.0.0" : raw.slice(second + 1);
|
|
37
|
-
} else {
|
|
38
|
-
const at = raw.indexOf("@");
|
|
39
|
-
name = at <= 0 ? raw : raw.slice(0, at);
|
|
40
|
-
version = at <= 0 ? "0.0.0" : raw.slice(at + 1);
|
|
41
|
-
}
|
|
42
|
-
return `pkg:npm/${encodeURIComponent(name)}@${version}`;
|
|
43
|
-
}
|
|
44
|
-
function notImported(finding) {
|
|
45
|
-
return finding.reachability === "not-imported";
|
|
46
|
-
}
|
|
47
|
-
function openVexStatement(finding) {
|
|
48
|
-
const base = {
|
|
49
|
-
vulnerability: { name: vulnName(finding) },
|
|
50
|
-
products: [{ "@id": npmPurl(finding.snippet) }]
|
|
51
|
-
};
|
|
52
|
-
const detail = process.env.FULLSHIELD_EXPERIMENTAL_SBOM_REACH === "1" && finding.reachabilityEvidence?.reason !== void 0 ? { detail: finding.reachabilityEvidence.reason } : {};
|
|
53
|
-
if (notImported(finding)) {
|
|
54
|
-
return {
|
|
55
|
-
...base,
|
|
56
|
-
status: "not_affected",
|
|
57
|
-
justification: OPENVEX_NOT_AFFECTED_JUSTIFICATION,
|
|
58
|
-
...detail
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
return { ...base, status: "under_investigation", ...detail };
|
|
62
|
-
}
|
|
63
|
-
function buildOpenVex(findings) {
|
|
64
|
-
const statements = depFindings(findings).map(openVexStatement);
|
|
65
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
66
|
-
const idSeed = JSON.stringify(statements);
|
|
67
|
-
const digest = createHash("sha256").update(idSeed).digest("hex").slice(0, 16);
|
|
68
|
-
return {
|
|
69
|
-
"@context": OPENVEX_CONTEXT,
|
|
70
|
-
"@id": `https://openvex.dev/docs/public/fullshield-${digest}`,
|
|
71
|
-
author: "FullShield",
|
|
72
|
-
timestamp,
|
|
73
|
-
version: 1,
|
|
74
|
-
statements
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
function validateOpenVex(doc) {
|
|
78
|
-
const errors = [];
|
|
79
|
-
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
80
|
-
return ["OpenVEX document must be an object"];
|
|
81
|
-
}
|
|
82
|
-
const root = doc;
|
|
83
|
-
if (typeof root["@context"] !== "string" || !String(root["@context"]).includes("openvex")) {
|
|
84
|
-
errors.push("@context must be an OpenVEX context URI");
|
|
85
|
-
}
|
|
86
|
-
if (typeof root["@id"] !== "string" || root["@id"].length === 0) errors.push("@id is required");
|
|
87
|
-
if (typeof root.author !== "string" || root.author.length === 0) errors.push("author is required");
|
|
88
|
-
if (typeof root.timestamp !== "string" || root.timestamp.length === 0) errors.push("timestamp is required");
|
|
89
|
-
if (typeof root.version !== "number") errors.push("version must be a number");
|
|
90
|
-
if (!Array.isArray(root.statements)) {
|
|
91
|
-
errors.push("statements must be an array");
|
|
92
|
-
return errors;
|
|
93
|
-
}
|
|
94
|
-
for (const [i, raw] of root.statements.entries()) {
|
|
95
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
96
|
-
errors.push(`statements[${i}] must be an object`);
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
const stmt = raw;
|
|
100
|
-
if (typeof stmt.vulnerability !== "object" || stmt.vulnerability === null) {
|
|
101
|
-
errors.push(`statements[${i}].vulnerability is required`);
|
|
102
|
-
}
|
|
103
|
-
if (!Array.isArray(stmt.products) || stmt.products.length === 0) {
|
|
104
|
-
errors.push(`statements[${i}].products must be a non-empty array`);
|
|
105
|
-
}
|
|
106
|
-
if (typeof stmt.status !== "string" || !OPENVEX_STATUSES.has(stmt.status)) {
|
|
107
|
-
errors.push(`statements[${i}].status is invalid`);
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (stmt.status === "not_affected") {
|
|
111
|
-
if (stmt.justification !== OPENVEX_NOT_AFFECTED_JUSTIFICATION) {
|
|
112
|
-
errors.push(
|
|
113
|
-
`statements[${i}]: not_affected requires justification ${OPENVEX_NOT_AFFECTED_JUSTIFICATION}`
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return errors;
|
|
119
|
-
}
|
|
120
|
-
function cdxAnalysis(finding) {
|
|
121
|
-
if (notImported(finding)) {
|
|
122
|
-
return { state: "not_affected", justification: CDX_NOT_AFFECTED_JUSTIFICATION };
|
|
123
|
-
}
|
|
124
|
-
return { state: "in_triage" };
|
|
125
|
-
}
|
|
126
|
-
function buildCycloneDxVex(meta, findings) {
|
|
127
|
-
return {
|
|
128
|
-
$schema: "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
129
|
-
bomFormat: "CycloneDX",
|
|
130
|
-
specVersion: "1.5",
|
|
131
|
-
version: 1,
|
|
132
|
-
metadata: {
|
|
133
|
-
component: { type: "application", name: meta.name, version: meta.version }
|
|
134
|
-
},
|
|
135
|
-
components: [],
|
|
136
|
-
vulnerabilities: depFindings(findings).map((f) => ({
|
|
137
|
-
id: vulnName(f),
|
|
138
|
-
description: f.message,
|
|
139
|
-
affects: [{ ref: npmPurl(f.snippet) }],
|
|
140
|
-
analysis: cdxAnalysis(f)
|
|
141
|
-
}))
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
function validateCycloneDxVex(doc) {
|
|
145
|
-
const errors = validateCycloneDx15(doc);
|
|
146
|
-
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return errors;
|
|
147
|
-
const root = doc;
|
|
148
|
-
if (!Array.isArray(root.vulnerabilities)) {
|
|
149
|
-
errors.push("vulnerabilities must be an array");
|
|
150
|
-
return errors;
|
|
151
|
-
}
|
|
152
|
-
for (const [i, raw] of root.vulnerabilities.entries()) {
|
|
153
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
154
|
-
errors.push(`vulnerabilities[${i}] must be an object`);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
const vuln = raw;
|
|
158
|
-
if (typeof vuln.analysis !== "object" || vuln.analysis === null || Array.isArray(vuln.analysis)) {
|
|
159
|
-
errors.push(`vulnerabilities[${i}].analysis is required`);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
const analysis = vuln.analysis;
|
|
163
|
-
if (typeof analysis.state !== "string" || analysis.state.length === 0) {
|
|
164
|
-
errors.push(`vulnerabilities[${i}].analysis.state is required`);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (analysis.state === "not_affected" && analysis.justification !== CDX_NOT_AFFECTED_JUSTIFICATION) {
|
|
168
|
-
errors.push(
|
|
169
|
-
`vulnerabilities[${i}]: not_affected requires justification ${CDX_NOT_AFFECTED_JUSTIFICATION}`
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return errors;
|
|
174
|
-
}
|
|
175
|
-
function buildVex(findings, format, meta = { name: "app", version: "0.0.0" }) {
|
|
176
|
-
if (format === "cyclonedx") return buildCycloneDxVex(meta, findings);
|
|
177
|
-
return buildOpenVex(findings);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export { buildCycloneDxVex, buildOpenVex, buildVex, validateCycloneDxVex, validateOpenVex };
|
|
1
|
+
import {createHash}from'crypto';function c(n){let e=[];if(typeof n!="object"||n===null||Array.isArray(n))return ["CycloneDX document must be an object"];let t=n;return t.bomFormat!=="CycloneDX"&&e.push('bomFormat must be "CycloneDX"'),t.specVersion!=="1.5"&&e.push('specVersion must be "1.5"'),Array.isArray(t.components)||e.push("components must be an array"),e}var y="https://openvex.dev/ns/v0.2.0",a="vulnerable_code_not_in_execute_path",u="code_not_reachable",g=new Set(["not_affected","affected","fixed","under_investigation"]);function l(n){return n.filter(e=>e.ruleId==="DEPS-VULN-001")}function p(n){return n.references?.find(e=>e.startsWith("CVE-")||e.startsWith("GHSA-")||e.startsWith("MAL-"))??n.id}function d(n){let e=n??"unknown@0.0.0",t,s;if(e.startsWith("@")){let i=e.indexOf("@",1);t=i===-1?e:e.slice(0,i),s=i===-1?"0.0.0":e.slice(i+1);}else {let i=e.indexOf("@");t=i<=0?e:e.slice(0,i),s=i<=0?"0.0.0":e.slice(i+1);}return `pkg:npm/${encodeURIComponent(t)}@${s}`}function m(n){return n.reachability==="not-imported"}function h(n){let e={vulnerability:{name:p(n)},products:[{"@id":d(n.snippet)}]},t=process.env.FULLSHIELD_EXPERIMENTAL_SBOM_REACH==="1"&&n.reachabilityEvidence?.reason!==void 0?{detail:n.reachabilityEvidence.reason}:{};return m(n)?{...e,status:"not_affected",justification:a,...t}:{...e,status:"under_investigation",...t}}function b(n){let e=l(n).map(h),t=new Date().toISOString(),s=JSON.stringify(e),i=createHash("sha256").update(s).digest("hex").slice(0,16);return {"@context":y,"@id":`https://openvex.dev/docs/public/fullshield-${i}`,author:"FullShield",timestamp:t,version:1,statements:e}}function E(n){let e=[];if(typeof n!="object"||n===null||Array.isArray(n))return ["OpenVEX document must be an object"];let t=n;if((typeof t["@context"]!="string"||!String(t["@context"]).includes("openvex"))&&e.push("@context must be an OpenVEX context URI"),(typeof t["@id"]!="string"||t["@id"].length===0)&&e.push("@id is required"),(typeof t.author!="string"||t.author.length===0)&&e.push("author is required"),(typeof t.timestamp!="string"||t.timestamp.length===0)&&e.push("timestamp is required"),typeof t.version!="number"&&e.push("version must be a number"),!Array.isArray(t.statements))return e.push("statements must be an array"),e;for(let[s,i]of t.statements.entries()){if(typeof i!="object"||i===null||Array.isArray(i)){e.push(`statements[${s}] must be an object`);continue}let r=i;if((typeof r.vulnerability!="object"||r.vulnerability===null)&&e.push(`statements[${s}].vulnerability is required`),(!Array.isArray(r.products)||r.products.length===0)&&e.push(`statements[${s}].products must be a non-empty array`),typeof r.status!="string"||!g.has(r.status)){e.push(`statements[${s}].status is invalid`);continue}r.status==="not_affected"&&r.justification!==a&&e.push(`statements[${s}]: not_affected requires justification ${a}`);}return e}function v(n){return m(n)?{state:"not_affected",justification:u}:{state:"in_triage"}}function x(n,e){return {$schema:"http://cyclonedx.org/schema/bom-1.5.schema.json",bomFormat:"CycloneDX",specVersion:"1.5",version:1,metadata:{component:{type:"application",name:n.name,version:n.version}},components:[],vulnerabilities:l(e).map(t=>({id:p(t),description:t.message,affects:[{ref:d(t.snippet)}],analysis:v(t)}))}}function j(n){let e=c(n);if(typeof n!="object"||n===null||Array.isArray(n))return e;let t=n;if(!Array.isArray(t.vulnerabilities))return e.push("vulnerabilities must be an array"),e;for(let[s,i]of t.vulnerabilities.entries()){if(typeof i!="object"||i===null||Array.isArray(i)){e.push(`vulnerabilities[${s}] must be an object`);continue}let r=i;if(typeof r.analysis!="object"||r.analysis===null||Array.isArray(r.analysis)){e.push(`vulnerabilities[${s}].analysis is required`);continue}let o=r.analysis;if(typeof o.state!="string"||o.state.length===0){e.push(`vulnerabilities[${s}].analysis.state is required`);continue}o.state==="not_affected"&&o.justification!==u&&e.push(`vulnerabilities[${s}]: not_affected requires justification ${u}`);}return e}function C(n,e,t={name:"app",version:"0.0.0"}){return e==="cyclonedx"?x(t,n):b(n)}
|
|
2
|
+
export{x as buildCycloneDxVex,b as buildOpenVex,C as buildVex,j as validateCycloneDxVex,E as validateOpenVex};
|
package/dist/workspaces.js
CHANGED
|
@@ -1,23 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// src/workspaces/index.ts
|
|
5
|
-
function listWorkspaces(dir, opts = {}) {
|
|
6
|
-
if (!opts.experimental && process.env.FULLSHIELD_EXPERIMENTAL_PLATFORM !== "1") return [];
|
|
7
|
-
const manifestPath = join(dir, "package.json");
|
|
8
|
-
if (!existsSync(manifestPath)) return [];
|
|
9
|
-
try {
|
|
10
|
-
const pkg = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
11
|
-
if (Array.isArray(pkg.workspaces)) return pkg.workspaces;
|
|
12
|
-
if (Array.isArray(pkg.workspaces?.packages)) return pkg.workspaces.packages;
|
|
13
|
-
} catch {
|
|
14
|
-
return [];
|
|
15
|
-
}
|
|
16
|
-
const pnpm = join(dir, "pnpm-workspace.yaml");
|
|
17
|
-
if (existsSync(pnpm)) {
|
|
18
|
-
return readFileSync(pnpm, "utf8").split("\n").map((l) => l.replace(/^-+\s*/, "").trim()).filter((l) => l.length > 0 && !l.startsWith("packages:"));
|
|
19
|
-
}
|
|
20
|
-
return [];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export { listWorkspaces };
|
|
1
|
+
import {existsSync,readFileSync}from'fs';import {join}from'path';function k(s,i={}){if(!i.experimental&&process.env.FULLSHIELD_EXPERIMENTAL_PLATFORM!=="1")return [];let e=join(s,"package.json");if(!existsSync(e))return [];try{let r=JSON.parse(readFileSync(e,"utf8"));if(Array.isArray(r.workspaces))return r.workspaces;if(Array.isArray(r.workspaces?.packages))return r.workspaces.packages}catch{return []}let t=join(s,"pnpm-workspace.yaml");return existsSync(t)?readFileSync(t,"utf8").split(`
|
|
2
|
+
`).map(r=>r.replace(/^-+\s*/,"").trim()).filter(r=>r.length>0&&!r.startsWith("packages:")):[]}export{k as listWorkspaces};
|