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/iac.js
CHANGED
|
@@ -1,132 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { createHash } from 'crypto';
|
|
4
|
-
|
|
5
|
-
// src/iac/index.ts
|
|
6
|
-
function makeFindingId(ruleId, file, snippet) {
|
|
7
|
-
const normalized = (snippet ?? "").replace(/\s+/g, " ").trim();
|
|
8
|
-
return createHash("sha256").update(`${ruleId}\0${file ?? ""}\0${normalized}`).digest("hex").slice(0, 16);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// src/iac/index.ts
|
|
12
|
-
function finding(ruleId, title, message, file, severity = "high") {
|
|
13
|
-
return {
|
|
14
|
-
id: makeFindingId(ruleId, file, message),
|
|
15
|
-
ruleId,
|
|
16
|
-
module: "config",
|
|
17
|
-
title,
|
|
18
|
-
severity,
|
|
19
|
-
confidence: "medium",
|
|
20
|
-
file,
|
|
21
|
-
message,
|
|
22
|
-
remediation: "Pin actions by SHA, drop pull_request_target, tighten permissions, close 0.0.0.0/0, encrypt storage, drop privileged."
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function runBodies(text) {
|
|
26
|
-
const bodies = [];
|
|
27
|
-
const lines = text.split(/\r?\n/);
|
|
28
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
29
|
-
const match = /^(\s*)(?:-\s+)?run:\s*(.*)$/.exec(lines[i] ?? "");
|
|
30
|
-
if (match === null) continue;
|
|
31
|
-
const indent = match[1]?.length ?? 0;
|
|
32
|
-
let body = match[2] ?? "";
|
|
33
|
-
if (body === "|" || body === ">") {
|
|
34
|
-
const block = [];
|
|
35
|
-
for (let j = i + 1; j < lines.length; j += 1) {
|
|
36
|
-
const line = lines[j] ?? "";
|
|
37
|
-
if (line.trim() === "") {
|
|
38
|
-
block.push(line);
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
const lead = line.match(/^\s*/)?.[0].length ?? 0;
|
|
42
|
-
if (lead <= indent) break;
|
|
43
|
-
block.push(line);
|
|
44
|
-
}
|
|
45
|
-
body = block.join("\n");
|
|
46
|
-
}
|
|
47
|
-
bodies.push(body);
|
|
48
|
-
}
|
|
49
|
-
return bodies;
|
|
50
|
-
}
|
|
51
|
-
function walk(dir, out = []) {
|
|
52
|
-
if (!existsSync(dir)) return out;
|
|
53
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
54
|
-
const full = join(dir, entry.name);
|
|
55
|
-
if (entry.isDirectory()) {
|
|
56
|
-
if (["node_modules", "dist", ".git"].includes(entry.name)) continue;
|
|
57
|
-
walk(full, out);
|
|
58
|
-
} else if (entry.isFile()) {
|
|
59
|
-
out.push(full);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return out;
|
|
63
|
-
}
|
|
64
|
-
function scanIac(dir, opts = {}) {
|
|
65
|
-
if (!opts.experimental && process.env.FULLSHIELD_EXPERIMENTAL_IAC !== "1") return [];
|
|
66
|
-
if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
|
|
67
|
-
const findings = [];
|
|
68
|
-
for (const file of walk(dir)) {
|
|
69
|
-
const rel = relative(dir, file).replaceAll("\\", "/");
|
|
70
|
-
const text = readFileSync(file, "utf8");
|
|
71
|
-
if (rel.includes(".github/workflows/") && /\.ya?ml$/.test(rel)) {
|
|
72
|
-
if (/uses:\s+\S+@v\d/.test(text) && !/uses:\s+\S+@[0-9a-f]{40}/.test(text)) {
|
|
73
|
-
findings.push(finding("IAC-GHA-UNPINNED-001", "GitHub Action not pinned to a commit SHA", "uses: tag instead of SHA", rel));
|
|
74
|
-
}
|
|
75
|
-
if (/pull_request_target/.test(text)) {
|
|
76
|
-
findings.push(finding("IAC-GHA-PRTARGET-001", "pull_request_target workflow", "untrusted PR can run with base secrets", rel, "critical"));
|
|
77
|
-
}
|
|
78
|
-
if (/pull_request_target/.test(text) && /actions\/checkout/.test(text) && /github\.event\.pull_request/.test(text)) {
|
|
79
|
-
findings.push(
|
|
80
|
-
finding(
|
|
81
|
-
"IAC-GHA-PRCHECKOUT-001",
|
|
82
|
-
"pull_request_target checks out pull-request code",
|
|
83
|
-
"untrusted head can run with the base repository token",
|
|
84
|
-
rel,
|
|
85
|
-
"critical"
|
|
86
|
-
)
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
if (runBodies(text).some((body) => /\$\{\{\s*github\.event\./.test(body))) {
|
|
90
|
-
findings.push(
|
|
91
|
-
finding(
|
|
92
|
-
"IAC-GHA-INJECT-001",
|
|
93
|
-
"Workflow run step interpolates github.event",
|
|
94
|
-
"untrusted event text is passed to the shell",
|
|
95
|
-
rel,
|
|
96
|
-
"critical"
|
|
97
|
-
)
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
if (/permissions:\s*\n\s+contents:\s*write/.test(text) || /permissions:\s+write-all/.test(text)) {
|
|
101
|
-
findings.push(finding("IAC-GHA-PERMS-001", "Overly broad GitHub Actions permissions", "write-all or contents: write", rel));
|
|
102
|
-
}
|
|
103
|
-
if (/echo\s+\$\{\{\s*secrets\.|console\.(?:log|info|debug)\([^)]*secrets\./i.test(text)) {
|
|
104
|
-
findings.push(finding("IAC-GHA-SECRETLOG-001", "Secret written to workflow logs", "echo or console of secrets.*", rel, "critical"));
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (/\.tf$/.test(rel)) {
|
|
108
|
-
if (/acl\s*=\s*"public-read"|block_public_acls\s*=\s*false/.test(text)) {
|
|
109
|
-
findings.push(finding("IAC-TF-S3PUBLIC-001", "Public S3 bucket", "public-read or public ACLs enabled", rel, "critical"));
|
|
110
|
-
}
|
|
111
|
-
if (/0\.0\.0\.0\/0/.test(text) && /22|3306|5432/.test(text)) {
|
|
112
|
-
findings.push(finding("IAC-TF-SGOPEN-001", "Security group open to the world on a data/admin port", "0.0.0.0/0 on 22/3306/5432", rel, "critical"));
|
|
113
|
-
}
|
|
114
|
-
if (/encrypted\s*=\s*false/.test(text)) {
|
|
115
|
-
findings.push(finding("IAC-TF-UNENCRYPTED-001", "Unencrypted cloud storage", "encrypted = false", rel));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (/deployment\.ya?ml$|pod\.ya?ml$/.test(rel) || rel.includes("k8s/")) {
|
|
119
|
-
if (/privileged:\s*true/.test(text) || /hostNetwork:\s*true/.test(text)) {
|
|
120
|
-
findings.push(finding("IAC-K8S-PRIV-001", "Privileged or hostNetwork Kubernetes pod", "privileged/hostNetwork true", rel, "critical"));
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (/compose\.ya?ml$|docker-compose\.ya?ml$/.test(rel)) {
|
|
124
|
-
if (/privileged:\s*true/.test(text)) {
|
|
125
|
-
findings.push(finding("IAC-COMPOSE-PRIV-001", "Privileged docker-compose service", "privileged: true", rel, "critical"));
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return findings;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export { scanIac };
|
|
1
|
+
import {existsSync,statSync,readFileSync,readdirSync}from'fs';import {relative,join}from'path';import {createHash}from'crypto';function d(n,s,e){let r=(e??"").replace(/\s+/g," ").trim();return createHash("sha256").update(`${n}\0${s??""}\0${r}`).digest("hex").slice(0,16)}function o(n,s,e,r,t="high"){return {id:d(n,r,e),ruleId:n,module:"config",title:s,severity:t,confidence:"medium",file:r,message:e,remediation:"Pin actions by SHA, drop pull_request_target, tighten permissions, close 0.0.0.0/0, encrypt storage, drop privileged."}}function v(n){let s=[],e=n.split(/\r?\n/);for(let r=0;r<e.length;r+=1){let t=/^(\s*)(?:-\s+)?run:\s*(.*)$/.exec(e[r]??"");if(t===null)continue;let i=t[1]?.length??0,c=t[2]??"";if(c==="|"||c===">"){let a=[];for(let u=r+1;u<e.length;u+=1){let l=e[u]??"";if(l.trim()===""){a.push(l);continue}if((l.match(/^\s*/)?.[0].length??0)<=i)break;a.push(l);}c=a.join(`
|
|
2
|
+
`);}s.push(c);}return s}function g(n,s=[]){if(!existsSync(n))return s;for(let e of readdirSync(n,{withFileTypes:true})){let r=join(n,e.name);if(e.isDirectory()){if(["node_modules","dist",".git"].includes(e.name))continue;g(r,s);}else e.isFile()&&s.push(r);}return s}function C(n,s={}){if(!s.experimental&&process.env.FULLSHIELD_EXPERIMENTAL_IAC!=="1")return [];if(!existsSync(n)||!statSync(n).isDirectory())return [];let e=[];for(let r of g(n)){let t=relative(n,r).replaceAll("\\","/"),i=readFileSync(r,"utf8");t.includes(".github/workflows/")&&/\.ya?ml$/.test(t)&&(/uses:\s+\S+@v\d/.test(i)&&!/uses:\s+\S+@[0-9a-f]{40}/.test(i)&&e.push(o("IAC-GHA-UNPINNED-001","GitHub Action not pinned to a commit SHA","uses: tag instead of SHA",t)),/pull_request_target/.test(i)&&e.push(o("IAC-GHA-PRTARGET-001","pull_request_target workflow","untrusted PR can run with base secrets",t,"critical")),/pull_request_target/.test(i)&&/actions\/checkout/.test(i)&&/github\.event\.pull_request/.test(i)&&e.push(o("IAC-GHA-PRCHECKOUT-001","pull_request_target checks out pull-request code","untrusted head can run with the base repository token",t,"critical")),v(i).some(c=>/\$\{\{\s*github\.event\./.test(c))&&e.push(o("IAC-GHA-INJECT-001","Workflow run step interpolates github.event","untrusted event text is passed to the shell",t,"critical")),(/permissions:\s*\n\s+contents:\s*write/.test(i)||/permissions:\s+write-all/.test(i))&&e.push(o("IAC-GHA-PERMS-001","Overly broad GitHub Actions permissions","write-all or contents: write",t)),/echo\s+\$\{\{\s*secrets\.|console\.(?:log|info|debug)\([^)]*secrets\./i.test(i)&&e.push(o("IAC-GHA-SECRETLOG-001","Secret written to workflow logs","echo or console of secrets.*",t,"critical"))),/\.tf$/.test(t)&&(/acl\s*=\s*"public-read"|block_public_acls\s*=\s*false/.test(i)&&e.push(o("IAC-TF-S3PUBLIC-001","Public S3 bucket","public-read or public ACLs enabled",t,"critical")),/0\.0\.0\.0\/0/.test(i)&&/22|3306|5432/.test(i)&&e.push(o("IAC-TF-SGOPEN-001","Security group open to the world on a data/admin port","0.0.0.0/0 on 22/3306/5432",t,"critical")),/encrypted\s*=\s*false/.test(i)&&e.push(o("IAC-TF-UNENCRYPTED-001","Unencrypted cloud storage","encrypted = false",t))),(/deployment\.ya?ml$|pod\.ya?ml$/.test(t)||t.includes("k8s/"))&&(/privileged:\s*true/.test(i)||/hostNetwork:\s*true/.test(i))&&e.push(o("IAC-K8S-PRIV-001","Privileged or hostNetwork Kubernetes pod","privileged/hostNetwork true",t,"critical")),/compose\.ya?ml$|docker-compose\.ya?ml$/.test(t)&&/privileged:\s*true/.test(i)&&e.push(o("IAC-COMPOSE-PRIV-001","Privileged docker-compose service","privileged: true",t,"critical"));}return e}export{C as scanIac};
|
package/dist/incremental.js
CHANGED
|
@@ -1,13 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
// src/incremental/index.ts
|
|
4
|
-
function resolveChangedFiles(cwd, opts = {}) {
|
|
5
|
-
if (!opts.experimental && process.env.FULLSHIELD_EXPERIMENTAL_PLATFORM !== "1") return [];
|
|
6
|
-
if (!opts.changed && !opts.since) return [];
|
|
7
|
-
const run = opts.git ?? ((args) => spawnSync("git", args, { cwd, encoding: "utf8" }).stdout ?? "");
|
|
8
|
-
const range = opts.since ?? "HEAD";
|
|
9
|
-
const out = opts.changed ? run(["diff", "--name-only", "--cached"]) : run(["diff", "--name-only", range]);
|
|
10
|
-
return out.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export { resolveChangedFiles };
|
|
1
|
+
import {spawnSync}from'child_process';function s(t,n={}){if(!n.experimental&&process.env.FULLSHIELD_EXPERIMENTAL_PLATFORM!=="1")return [];if(!n.changed&&!n.since)return [];let i=n.git??(e=>spawnSync("git",e,{cwd:t,encoding:"utf8"}).stdout??""),r=n.since??"HEAD";return (n.changed?i(["diff","--name-only","--cached"]):i(["diff","--name-only",r])).split(`
|
|
2
|
+
`).map(e=>e.trim()).filter(e=>e.length>0)}export{s as resolveChangedFiles};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import * as zod from 'zod';
|
|
2
2
|
|
|
3
|
+
declare const CAPABILITY_NAMES: readonly ["child_process", "net", "fs_write", "env", "eval", "native", "dynamic_require", "install_script", "obfuscation"];
|
|
4
|
+
type CapabilityName = (typeof CAPABILITY_NAMES)[number];
|
|
5
|
+
|
|
6
|
+
interface CapabilityPolicy {
|
|
7
|
+
deny: Partial<Record<CapabilityName, readonly string[]>>;
|
|
8
|
+
warn: Partial<Record<CapabilityName, readonly string[]>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
declare const SEVERITIES: readonly ["info", "low", "medium", "high", "critical"];
|
|
4
12
|
type Severity = (typeof SEVERITIES)[number];
|
|
5
13
|
declare const MODULE_NAMES: readonly ["deps", "secrets", "sast", "config"];
|
|
6
14
|
type ModuleName = (typeof MODULE_NAMES)[number];
|
|
7
15
|
type Confidence = 'low' | 'medium' | 'high';
|
|
16
|
+
/** Built-in `Finding.source` tags. Any other ingest tool name is stored as `sarif`. */
|
|
17
|
+
declare const FINDING_SOURCES: readonly ["deterministic", "ai", "user-rule", "npm-audit", "semgrep", "osv-scanner", "sarif"];
|
|
18
|
+
type FindingSource = (typeof FINDING_SOURCES)[number];
|
|
19
|
+
/** Why a finding was moved to `ScanResult.suppressed`. */
|
|
20
|
+
type SuppressedBy = 'types' | `model:${string}`;
|
|
8
21
|
interface FixProposal {
|
|
9
22
|
kind: 'dependency-bump' | 'code-patch' | 'config-add' | 'dependency-override';
|
|
10
23
|
/** Only safe fixes apply without --unsafe. */
|
|
@@ -47,8 +60,20 @@ interface Finding {
|
|
|
47
60
|
dependencyPath?: string[];
|
|
48
61
|
/** Every retained dependency path for this package+advisory (shortest first). */
|
|
49
62
|
dependencyPaths?: string[][];
|
|
50
|
-
/**
|
|
51
|
-
source?:
|
|
63
|
+
/** Built-in tag. An unknown ingest tool name is stored as `sarif`; the original name stays in `sources`. */
|
|
64
|
+
source?: FindingSource;
|
|
65
|
+
/** Every tool that agreed on an ingested row. Ingest only. */
|
|
66
|
+
sources?: string[];
|
|
67
|
+
/** Original rule id from an external tool. Ingest only. */
|
|
68
|
+
externalRuleId?: string;
|
|
69
|
+
/** Every original rule or advisory id on an ingested row. */
|
|
70
|
+
externalRuleIds?: string[];
|
|
71
|
+
/** Severity token from the external tool, unchanged. Ingest only. */
|
|
72
|
+
externalSeverity?: string;
|
|
73
|
+
/** Sink class copied from a report that already has one. Ingest only. */
|
|
74
|
+
sinkClass?: string;
|
|
75
|
+
/** Route exposure copied from a report that already has it. Ingest only. */
|
|
76
|
+
routeExposure?: 'unauthenticated' | 'authenticated' | 'unknown';
|
|
52
77
|
aiVerdict?: 'true-positive' | 'likely-false-positive' | 'needs-review';
|
|
53
78
|
aiReasoning?: string;
|
|
54
79
|
exploitScenario?: string;
|
|
@@ -59,6 +84,27 @@ interface Finding {
|
|
|
59
84
|
stillAtHead?: boolean;
|
|
60
85
|
/** Read-only identity check (`scan --verify-secrets`). Never includes the secret. */
|
|
61
86
|
verification?: 'live' | 'revoked' | 'unknown';
|
|
87
|
+
/** 0–100. Present only with FULLSHIELD_EXPERIMENTAL_PRIORITY=1. Severity is never changed. */
|
|
88
|
+
priority?: number;
|
|
89
|
+
/** Why `priority` was chosen. Same flag as `priority`. */
|
|
90
|
+
priorityFactors?: Array<{
|
|
91
|
+
name: string;
|
|
92
|
+
points: number;
|
|
93
|
+
detail: string;
|
|
94
|
+
}>;
|
|
95
|
+
/**
|
|
96
|
+
* Offline structure check. Present only with FULLSHIELD_EXPERIMENTAL_SECRET_CHECK=1.
|
|
97
|
+
* Never deletes the finding and never changes severity.
|
|
98
|
+
*/
|
|
99
|
+
secretCheck?: 'checksum-failed' | 'placeholder';
|
|
100
|
+
/** Set to `test` for test, fixture, example, and sample paths. Flag-only. */
|
|
101
|
+
context?: 'test';
|
|
102
|
+
/**
|
|
103
|
+
* Set on copies listed in `ScanResult.suppressed`.
|
|
104
|
+
* `types` is the TypeScript receiver check. `model:<package>` is a library model pack.
|
|
105
|
+
* Absent on findings that are still reported.
|
|
106
|
+
*/
|
|
107
|
+
suppressedBy?: SuppressedBy;
|
|
62
108
|
/** Why the reachability class was chosen. Present only with FULLSHIELD_EXPERIMENTAL_REACH_EVIDENCE=1. */
|
|
63
109
|
reachabilityEvidence?: {
|
|
64
110
|
imports: Array<{
|
|
@@ -101,6 +147,8 @@ interface DepsConfig {
|
|
|
101
147
|
offline: boolean;
|
|
102
148
|
/** Cache root (contains an `osv/` directory). Relative paths are resolved from cwd. */
|
|
103
149
|
cacheDir?: string;
|
|
150
|
+
/** Present only when FULLSHIELD_EXPERIMENTAL_CAPABILITIES=1. */
|
|
151
|
+
capabilityPolicy?: CapabilityPolicy;
|
|
104
152
|
}
|
|
105
153
|
interface AiConfig {
|
|
106
154
|
/** Off by default. Also enabled by `--ai`. */
|
|
@@ -125,6 +173,10 @@ interface SastConfig {
|
|
|
125
173
|
sources?: SastSourceConfig[];
|
|
126
174
|
sanitizers?: SastSanitizerConfig[];
|
|
127
175
|
}
|
|
176
|
+
/** Phase 80. The TypeScript program is not built above `maxFiles`. */
|
|
177
|
+
interface TypesConfig {
|
|
178
|
+
maxFiles: number;
|
|
179
|
+
}
|
|
128
180
|
interface ResolvedConfig {
|
|
129
181
|
modules: ModuleName[];
|
|
130
182
|
failOn: Severity;
|
|
@@ -138,6 +190,9 @@ interface ResolvedConfig {
|
|
|
138
190
|
deps: DepsConfig;
|
|
139
191
|
ai: AiConfig;
|
|
140
192
|
sast: SastConfig;
|
|
193
|
+
types: TypesConfig;
|
|
194
|
+
/** Phase 90. Path to a user Semgrep-style YAML file. Read only when the experimental flag is on. */
|
|
195
|
+
semgrepRules?: string;
|
|
141
196
|
}
|
|
142
197
|
declare const DEFAULT_CONFIG: ResolvedConfig;
|
|
143
198
|
declare class ConfigError extends Error {
|
|
@@ -148,6 +203,30 @@ declare class ConfigError extends Error {
|
|
|
148
203
|
*/
|
|
149
204
|
declare function loadConfig(cwd: string, configPath?: string): Promise<ResolvedConfig>;
|
|
150
205
|
|
|
206
|
+
interface AttackPathEntry {
|
|
207
|
+
method: string;
|
|
208
|
+
path: string;
|
|
209
|
+
auth: 'present' | 'absent';
|
|
210
|
+
}
|
|
211
|
+
interface AttackPathStep {
|
|
212
|
+
file: string;
|
|
213
|
+
line?: number;
|
|
214
|
+
note: string;
|
|
215
|
+
}
|
|
216
|
+
interface AttackPath {
|
|
217
|
+
kind: 'sink' | 'dependency' | 'secret';
|
|
218
|
+
entry?: AttackPathEntry;
|
|
219
|
+
steps: AttackPathStep[];
|
|
220
|
+
sink: {
|
|
221
|
+
id: string;
|
|
222
|
+
ruleId: string;
|
|
223
|
+
severity: Severity;
|
|
224
|
+
file?: string;
|
|
225
|
+
line?: number;
|
|
226
|
+
};
|
|
227
|
+
findingIds: string[];
|
|
228
|
+
}
|
|
229
|
+
|
|
151
230
|
/** Stable GitHub URL for a shipped rule doc (also valid as a GitLab-comment link). */
|
|
152
231
|
declare function ruleDocsUrl(ruleId: string): string;
|
|
153
232
|
/**
|
|
@@ -248,6 +327,15 @@ interface ScanOptions {
|
|
|
248
327
|
experimentalPolicy?: boolean;
|
|
249
328
|
/** Override git/CI branch when applying `failOn` per branch. */
|
|
250
329
|
policyBranch?: string;
|
|
330
|
+
/** Phase 85: join existing findings into attack paths. Requires FULLSHIELD_EXPERIMENTAL_PATHS=1. */
|
|
331
|
+
paths?: boolean;
|
|
332
|
+
/**
|
|
333
|
+
* Phase 79. Seconds for `--max-time`. Applied only when
|
|
334
|
+
* `FULLSHIELD_EXPERIMENTAL_BUDGET=1`.
|
|
335
|
+
*/
|
|
336
|
+
maxTimeSeconds?: number;
|
|
337
|
+
/** Phase 90. Overrides `semgrepRules` in config. The file is parsed only when the flag is on. */
|
|
338
|
+
semgrepRules?: string;
|
|
251
339
|
}
|
|
252
340
|
interface ScanMetadata {
|
|
253
341
|
version: string;
|
|
@@ -255,9 +343,9 @@ interface ScanMetadata {
|
|
|
255
343
|
durationMs: number;
|
|
256
344
|
cwd: string;
|
|
257
345
|
filesScanned: number;
|
|
258
|
-
modulesRequested: ModuleName[];
|
|
346
|
+
modulesRequested: readonly ModuleName[];
|
|
259
347
|
/** Requested modules that are actually implemented and ran. */
|
|
260
|
-
modulesRun: ModuleName[];
|
|
348
|
+
modulesRun: readonly ModuleName[];
|
|
261
349
|
/** Set when `scan --history` stops at the commit cap. Exact text: `scanned N of M commits`. */
|
|
262
350
|
history?: string;
|
|
263
351
|
/** Phase 48. Direct dependencies no project file imports. Absent when the list is empty. */
|
|
@@ -265,6 +353,26 @@ interface ScanMetadata {
|
|
|
265
353
|
packages: string[];
|
|
266
354
|
advisories: number;
|
|
267
355
|
};
|
|
356
|
+
/** Phase 81. One line per model pack skipped because the installed version is out of range. */
|
|
357
|
+
info?: string[];
|
|
358
|
+
/** Phase 78. Present only when FULLSHIELD_EXPERIMENTAL_PERF2=1. Not findings. */
|
|
359
|
+
skipped?: {
|
|
360
|
+
total: number;
|
|
361
|
+
oversize: number;
|
|
362
|
+
minified: number;
|
|
363
|
+
declaration: number;
|
|
364
|
+
sourceMap: number;
|
|
365
|
+
generated: number;
|
|
366
|
+
};
|
|
367
|
+
/**
|
|
368
|
+
* Phase 79. Set only when `FULLSHIELD_EXPERIMENTAL_BUDGET` stops the scan early.
|
|
369
|
+
* An incomplete scan is not a clean result.
|
|
370
|
+
*/
|
|
371
|
+
incomplete?: true;
|
|
372
|
+
/** Modules that did not finish. Deps, secrets, and config still run to completion. */
|
|
373
|
+
incompleteModules?: ModuleName[];
|
|
374
|
+
/** Source files that did not get a SAST taint pass. */
|
|
375
|
+
filesNotTaintChecked?: number;
|
|
268
376
|
/** Phase 49. Direct version bumps taken from OSV `fixed` versions. Absent when there is nothing to say. */
|
|
269
377
|
upgradePlan?: {
|
|
270
378
|
bumps: Array<{
|
|
@@ -294,6 +402,15 @@ interface ScanResult {
|
|
|
294
402
|
policy?: PolicyScanResult;
|
|
295
403
|
/** Set when a baseline suppression is missing fields or has expired. */
|
|
296
404
|
suppressionErrors?: string[];
|
|
405
|
+
/**
|
|
406
|
+
* Phase 80 and Phase 81. Findings removed from `findings`.
|
|
407
|
+
* Present when the type checker ran or a model pack suppressed a finding.
|
|
408
|
+
*/
|
|
409
|
+
suppressed?: Finding[];
|
|
410
|
+
/** Phase 80. Missing `typescript` (FS-E013) or the types.maxFiles fallback. Printed by the CLI. */
|
|
411
|
+
typesNotice?: string;
|
|
412
|
+
/** Phase 85. Present only when scan paths were requested and FULLSHIELD_EXPERIMENTAL_PATHS=1. */
|
|
413
|
+
paths?: AttackPath[];
|
|
297
414
|
}
|
|
298
415
|
/**
|
|
299
416
|
* Scanner modules register here as their phases are built.
|
|
@@ -335,6 +452,8 @@ declare function walkFiles(options: WalkOptions): Promise<string[]>;
|
|
|
335
452
|
*/
|
|
336
453
|
declare function packageVersion(): string;
|
|
337
454
|
|
|
455
|
+
type FixVerification = 'removes-finding' | 'does-not-remove' | 'introduces-finding';
|
|
456
|
+
|
|
338
457
|
interface FixOptions {
|
|
339
458
|
cwd?: string;
|
|
340
459
|
modules?: ModuleName[];
|
|
@@ -342,12 +461,19 @@ interface FixOptions {
|
|
|
342
461
|
force?: boolean;
|
|
343
462
|
unsafe?: boolean;
|
|
344
463
|
noBranch?: boolean;
|
|
464
|
+
/** Re-scan each patch in memory. Also on when FULLSHIELD_EXPERIMENTAL_FIX_VERIFY=1. */
|
|
465
|
+
experimentalFixVerify?: boolean;
|
|
466
|
+
/** Write a patch whose rescan still reports the original finding. */
|
|
467
|
+
forceUnverified?: boolean;
|
|
468
|
+
/** Accepted by the CLI. It does not write a patch that introduces a finding. */
|
|
469
|
+
yes?: boolean;
|
|
345
470
|
}
|
|
346
471
|
interface FixPatch {
|
|
347
472
|
file: string;
|
|
348
473
|
ruleId: string;
|
|
349
474
|
safe: boolean;
|
|
350
475
|
diff: string;
|
|
476
|
+
verified?: FixVerification;
|
|
351
477
|
}
|
|
352
478
|
interface FixResult {
|
|
353
479
|
applied: boolean;
|
|
@@ -357,6 +483,8 @@ interface FixResult {
|
|
|
357
483
|
findings: Finding[];
|
|
358
484
|
/** Set when an override was written to package.json. The lockfile is left unchanged. */
|
|
359
485
|
notice?: string;
|
|
486
|
+
/** Set when a patch does not remove its finding or would introduce one. */
|
|
487
|
+
warnings?: string[];
|
|
360
488
|
}
|
|
361
489
|
declare function runFix(options?: FixOptions): Promise<FixResult>;
|
|
362
490
|
|
|
@@ -579,6 +707,41 @@ declare function buildVex(findings: readonly Finding[], format: VexFormat, meta?
|
|
|
579
707
|
/** Phase 31 worm / VEX / overrides / provenance. Off unless the env flag or CLI option is set. */
|
|
580
708
|
declare function experimentalVex(): boolean;
|
|
581
709
|
|
|
710
|
+
/** Phase 84. Off unless FULLSHIELD_EXPERIMENTAL_PRIORITY=1 or --experimental-priority. */
|
|
711
|
+
declare const PRIORITY_FLAG = "FULLSHIELD_EXPERIMENTAL_PRIORITY";
|
|
712
|
+
declare function priorityEnabled(): boolean;
|
|
713
|
+
type RouteExposure = 'unauthenticated' | 'authenticated' | 'unknown';
|
|
714
|
+
interface PriorityFactor {
|
|
715
|
+
name: 'always' | 'severity' | 'confidence' | 'reachability' | 'route' | 'devOnly' | 'sink' | 'sources';
|
|
716
|
+
points: number;
|
|
717
|
+
detail: string;
|
|
718
|
+
}
|
|
719
|
+
interface PriorityInput {
|
|
720
|
+
ruleId: string;
|
|
721
|
+
module?: ModuleName;
|
|
722
|
+
severity: Severity;
|
|
723
|
+
confidence: Confidence;
|
|
724
|
+
reachability?: Finding['reachability'];
|
|
725
|
+
routeExposure?: RouteExposure;
|
|
726
|
+
devOnly?: boolean;
|
|
727
|
+
/** Explicit class wins over the rule-id map. Omit it to derive. */
|
|
728
|
+
sinkClass?: string;
|
|
729
|
+
/** Phase 83. Omitted or 1 means a single source. */
|
|
730
|
+
agreeingSources?: number;
|
|
731
|
+
}
|
|
732
|
+
interface PriorityScore {
|
|
733
|
+
priority: number;
|
|
734
|
+
factors: PriorityFactor[];
|
|
735
|
+
}
|
|
736
|
+
/** Documented table in docs/priority.md. Same input always returns the same score. */
|
|
737
|
+
declare function scorePriority(input: PriorityInput): PriorityScore;
|
|
738
|
+
/** Score a finding. Does not change severity. Phase 56 data is not read. */
|
|
739
|
+
declare function scoreFinding(finding: Finding): PriorityScore;
|
|
740
|
+
/** Attach priority fields only when the flag is on. Order and severity stay as given. */
|
|
741
|
+
declare function applyPriority(findings: readonly Finding[]): Finding[];
|
|
742
|
+
/** Console order when the flag is on: priority, then severity, then file. */
|
|
743
|
+
declare function compareByPriority(a: Finding, b: Finding): number;
|
|
744
|
+
|
|
582
745
|
/** Static worm indicators: token read + publish / registry PUT / GitHub create / workflow write. */
|
|
583
746
|
declare function analyzeWormIndicators(dir: string): Finding[];
|
|
584
747
|
/** Static: install scripts or code writing .github/, .git/hooks/, or CI config. */
|
|
@@ -595,4 +758,27 @@ declare function provenanceCoverage(packages: readonly {
|
|
|
595
758
|
version: string;
|
|
596
759
|
}[], known: ReadonlyMap<string, boolean>): ProvenanceScore;
|
|
597
760
|
|
|
598
|
-
|
|
761
|
+
/** Local ingest failed for one file. Other files are still read. */
|
|
762
|
+
declare class IngestError extends Error {
|
|
763
|
+
readonly code = "FS-E012";
|
|
764
|
+
readonly file: string;
|
|
765
|
+
readonly schemaError: string;
|
|
766
|
+
constructor(file: string, schemaError: string);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
declare const INGEST_FLAG = "FULLSHIELD_EXPERIMENTAL_INGEST";
|
|
770
|
+
/** Off unless the env flag or `--experimental-ingest` is set. */
|
|
771
|
+
declare function ingestEnabled(explicit?: boolean): boolean;
|
|
772
|
+
|
|
773
|
+
interface IngestGroup {
|
|
774
|
+
source: string;
|
|
775
|
+
findings: Finding[];
|
|
776
|
+
}
|
|
777
|
+
interface IngestLoad {
|
|
778
|
+
findings: Finding[];
|
|
779
|
+
groups: IngestGroup[];
|
|
780
|
+
errors: IngestError[];
|
|
781
|
+
}
|
|
782
|
+
declare function ingestLocalFiles(files: readonly string[]): Promise<IngestLoad>;
|
|
783
|
+
|
|
784
|
+
export { type AiConfig, type AppliedPolicy, type BaselineFile, CONFIG_FILE_NAME, type Confidence, ConfigError, DEFAULT_CONFIG, DIFF_FLAG, type DepsConfig, type Finding, type FindingSummary, type FixOptions, type FixPatch, type FixProposal, type FixResult, type FullShieldPolicy, type GuardTool, type GuardToolCall, IMPLEMENTED_MODULES, INGEST_FLAG, IngestError, type IngestLoad, MODULE_NAMES, type McpScanOptions, type ModuleContext, type ModuleName, type NotifyConfig, POLICY_FILE_NAME, POLICY_FLAG, POLICY_PEER_ERROR, PRIORITY_FLAG, PolicyError, type PolicyException, type PolicyScanResult, type PriorityFactor, type PriorityInput, type PriorityScore, type ResolvedConfig, type RouteExposure, SEVERITIES, type SbomComponent, type SbomInput, type ScanDiff, type ScanMetadata, type ScanModule, type ScanOptions, type ScanResult, type Severity, type WalkOptions, analyzeWorkflowTampering, analyzeWormIndicators, applyBaseline, applyInlineSuppressions, applyPolicy, applyPriority, buildCycloneDxVex, buildOpenVex, buildSbom, buildVex, builtinModules, compareByPriority, createAiClient, diffScans, experimentalVex, failsCi, guardTools, ingestEnabled, ingestLocalFiles, isInternalAi, isLiveAi, isModuleName, isPolicyEnabled, isSeverity, loadConfig, loadPolicy, makeFindingId, mcpCfgEnabled, mcpHomeConfigPaths, meetsThreshold, packageVersion, parseIgnore, parsePolicy, parseScanDocument, priorityEnabled, provenanceCoverage, readBaseline, renderConsoleReport, renderCycloneDx, renderDiffText, renderGithubAnnotations, renderGitlabCodeQuality, renderHtmlReport, renderJsonReport, renderJunitReport, renderMarkdownReport, renderPrSummary, renderSarifReport, ruleDocsUrl, runAiFix, runFix, runScan, scan, scanMcpConfig, scanTextForSecrets, scoreFinding, scorePriority, severityRank, sortFindings, summarize, validateCycloneDx15, validateCycloneDxVex, validateOpenVex, validateSarif21, walkFiles, writeBaseline };
|