fullshield 0.3.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 +96 -0
- package/README.md +18 -6
- package/dist/agent.js +50 -9395
- package/dist/bench/score.js +2 -118
- package/dist/cli/index.js +133 -13749
- package/dist/compliance.js +2 -160
- 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 +245 -8
- package/dist/index.js +94 -10405
- package/dist/llm.js +2 -105
- package/dist/mcp.js +48 -9505
- 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 -178
- package/dist/workspaces.js +2 -23
- package/models/index.json +356 -0
- package/package.json +23 -8
- package/rules/DEPS-LOCKMISMATCH-001.md +7 -0
- package/rules/DEPS-PHANTOM-001.md +7 -0
- package/rules/README.md +3 -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-CHAINC2-001.md +3 -0
- package/rules/SUPPLY-PLATFORMGATE-001.md +3 -0
- package/rules/SUPPLY-PROV-001.md +15 -0
- package/rules/SUPPLY-SECHOLDER-001.md +3 -0
- package/rules/SUPPLY-STEALER-001.md +3 -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,42 @@ 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;
|
|
108
|
+
/** Why the reachability class was chosen. Present only with FULLSHIELD_EXPERIMENTAL_REACH_EVIDENCE=1. */
|
|
109
|
+
reachabilityEvidence?: {
|
|
110
|
+
imports: Array<{
|
|
111
|
+
file: string;
|
|
112
|
+
line: number;
|
|
113
|
+
specifier: string;
|
|
114
|
+
}>;
|
|
115
|
+
calls?: Array<{
|
|
116
|
+
file: string;
|
|
117
|
+
line: number;
|
|
118
|
+
symbol: string;
|
|
119
|
+
}>;
|
|
120
|
+
dependencyPath?: string[];
|
|
121
|
+
reason: string;
|
|
122
|
+
};
|
|
62
123
|
}
|
|
63
124
|
interface FindingSummary {
|
|
64
125
|
total: number;
|
|
@@ -86,6 +147,8 @@ interface DepsConfig {
|
|
|
86
147
|
offline: boolean;
|
|
87
148
|
/** Cache root (contains an `osv/` directory). Relative paths are resolved from cwd. */
|
|
88
149
|
cacheDir?: string;
|
|
150
|
+
/** Present only when FULLSHIELD_EXPERIMENTAL_CAPABILITIES=1. */
|
|
151
|
+
capabilityPolicy?: CapabilityPolicy;
|
|
89
152
|
}
|
|
90
153
|
interface AiConfig {
|
|
91
154
|
/** Off by default. Also enabled by `--ai`. */
|
|
@@ -95,9 +158,24 @@ interface AiConfig {
|
|
|
95
158
|
/** Highest-severity findings sent to the model first. */
|
|
96
159
|
maxFindings: number;
|
|
97
160
|
}
|
|
161
|
+
interface SastSourceConfig {
|
|
162
|
+
callee: string;
|
|
163
|
+
returns: 'tainted';
|
|
164
|
+
}
|
|
165
|
+
interface SastSanitizerConfig {
|
|
166
|
+
callee: string;
|
|
167
|
+
clears: string[];
|
|
168
|
+
}
|
|
98
169
|
interface SastConfig {
|
|
99
170
|
/** Files larger than this are not parsed. Reported as SAST-SKIP-001. */
|
|
100
171
|
maxFileBytes: number;
|
|
172
|
+
/** Present only when FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS=1. */
|
|
173
|
+
sources?: SastSourceConfig[];
|
|
174
|
+
sanitizers?: SastSanitizerConfig[];
|
|
175
|
+
}
|
|
176
|
+
/** Phase 80. The TypeScript program is not built above `maxFiles`. */
|
|
177
|
+
interface TypesConfig {
|
|
178
|
+
maxFiles: number;
|
|
101
179
|
}
|
|
102
180
|
interface ResolvedConfig {
|
|
103
181
|
modules: ModuleName[];
|
|
@@ -112,6 +190,9 @@ interface ResolvedConfig {
|
|
|
112
190
|
deps: DepsConfig;
|
|
113
191
|
ai: AiConfig;
|
|
114
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;
|
|
115
196
|
}
|
|
116
197
|
declare const DEFAULT_CONFIG: ResolvedConfig;
|
|
117
198
|
declare class ConfigError extends Error {
|
|
@@ -122,6 +203,30 @@ declare class ConfigError extends Error {
|
|
|
122
203
|
*/
|
|
123
204
|
declare function loadConfig(cwd: string, configPath?: string): Promise<ResolvedConfig>;
|
|
124
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
|
+
|
|
125
230
|
/** Stable GitHub URL for a shipped rule doc (also valid as a GitLab-comment link). */
|
|
126
231
|
declare function ruleDocsUrl(ruleId: string): string;
|
|
127
232
|
/**
|
|
@@ -222,6 +327,15 @@ interface ScanOptions {
|
|
|
222
327
|
experimentalPolicy?: boolean;
|
|
223
328
|
/** Override git/CI branch when applying `failOn` per branch. */
|
|
224
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;
|
|
225
339
|
}
|
|
226
340
|
interface ScanMetadata {
|
|
227
341
|
version: string;
|
|
@@ -229,11 +343,48 @@ interface ScanMetadata {
|
|
|
229
343
|
durationMs: number;
|
|
230
344
|
cwd: string;
|
|
231
345
|
filesScanned: number;
|
|
232
|
-
modulesRequested: ModuleName[];
|
|
346
|
+
modulesRequested: readonly ModuleName[];
|
|
233
347
|
/** Requested modules that are actually implemented and ran. */
|
|
234
|
-
modulesRun: ModuleName[];
|
|
348
|
+
modulesRun: readonly ModuleName[];
|
|
235
349
|
/** Set when `scan --history` stops at the commit cap. Exact text: `scanned N of M commits`. */
|
|
236
350
|
history?: string;
|
|
351
|
+
/** Phase 48. Direct dependencies no project file imports. Absent when the list is empty. */
|
|
352
|
+
unusedDirect?: {
|
|
353
|
+
packages: string[];
|
|
354
|
+
advisories: number;
|
|
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;
|
|
376
|
+
/** Phase 49. Direct version bumps taken from OSV `fixed` versions. Absent when there is nothing to say. */
|
|
377
|
+
upgradePlan?: {
|
|
378
|
+
bumps: Array<{
|
|
379
|
+
name: string;
|
|
380
|
+
from: string;
|
|
381
|
+
to: string;
|
|
382
|
+
safe: boolean;
|
|
383
|
+
findings: number;
|
|
384
|
+
}>;
|
|
385
|
+
unresolved: string[];
|
|
386
|
+
sentence: string;
|
|
387
|
+
};
|
|
237
388
|
}
|
|
238
389
|
interface PolicyScanResult {
|
|
239
390
|
enabled: true;
|
|
@@ -251,6 +402,15 @@ interface ScanResult {
|
|
|
251
402
|
policy?: PolicyScanResult;
|
|
252
403
|
/** Set when a baseline suppression is missing fields or has expired. */
|
|
253
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[];
|
|
254
414
|
}
|
|
255
415
|
/**
|
|
256
416
|
* Scanner modules register here as their phases are built.
|
|
@@ -292,6 +452,8 @@ declare function walkFiles(options: WalkOptions): Promise<string[]>;
|
|
|
292
452
|
*/
|
|
293
453
|
declare function packageVersion(): string;
|
|
294
454
|
|
|
455
|
+
type FixVerification = 'removes-finding' | 'does-not-remove' | 'introduces-finding';
|
|
456
|
+
|
|
295
457
|
interface FixOptions {
|
|
296
458
|
cwd?: string;
|
|
297
459
|
modules?: ModuleName[];
|
|
@@ -299,12 +461,19 @@ interface FixOptions {
|
|
|
299
461
|
force?: boolean;
|
|
300
462
|
unsafe?: boolean;
|
|
301
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;
|
|
302
470
|
}
|
|
303
471
|
interface FixPatch {
|
|
304
472
|
file: string;
|
|
305
473
|
ruleId: string;
|
|
306
474
|
safe: boolean;
|
|
307
475
|
diff: string;
|
|
476
|
+
verified?: FixVerification;
|
|
308
477
|
}
|
|
309
478
|
interface FixResult {
|
|
310
479
|
applied: boolean;
|
|
@@ -314,6 +483,8 @@ interface FixResult {
|
|
|
314
483
|
findings: Finding[];
|
|
315
484
|
/** Set when an override was written to package.json. The lockfile is left unchanged. */
|
|
316
485
|
notice?: string;
|
|
486
|
+
/** Set when a patch does not remove its finding or would introduce one. */
|
|
487
|
+
warnings?: string[];
|
|
317
488
|
}
|
|
318
489
|
declare function runFix(options?: FixOptions): Promise<FixResult>;
|
|
319
490
|
|
|
@@ -443,8 +614,12 @@ declare function parseIgnore(line: string): {
|
|
|
443
614
|
*/
|
|
444
615
|
declare function applyInlineSuppressions(findings: readonly Finding[], sources: Readonly<Record<string, string>>): Finding[];
|
|
445
616
|
|
|
446
|
-
|
|
447
|
-
|
|
617
|
+
interface ConsoleReportOptions {
|
|
618
|
+
/** Print every finding, with no 20-row cap. */
|
|
619
|
+
all?: boolean;
|
|
620
|
+
}
|
|
621
|
+
/** Human-readable report. Default view is the actionable rows. JSON is unchanged. */
|
|
622
|
+
declare function renderConsoleReport(input: ScanResult, options?: ConsoleReportOptions): string;
|
|
448
623
|
|
|
449
624
|
/** Standalone HTML report. Finding text is escaped; secrets are already masked. */
|
|
450
625
|
declare function renderHtmlReport(input: ScanResult): string;
|
|
@@ -495,7 +670,9 @@ declare function parseScanDocument(text: string): DiffFinding[];
|
|
|
495
670
|
/** Human summary. Lists at most 20 added findings. */
|
|
496
671
|
declare function renderDiffText(diff: ScanDiff): string;
|
|
497
672
|
|
|
498
|
-
declare function buildSbom(cwd: string
|
|
673
|
+
declare function buildSbom(cwd: string, options?: {
|
|
674
|
+
includeReach?: boolean;
|
|
675
|
+
}): Promise<Record<string, unknown>>;
|
|
499
676
|
|
|
500
677
|
interface SbomComponent {
|
|
501
678
|
name: string;
|
|
@@ -508,6 +685,8 @@ interface SbomInput {
|
|
|
508
685
|
version: string;
|
|
509
686
|
components: SbomComponent[];
|
|
510
687
|
findings?: Finding[];
|
|
688
|
+
/** Phase 66. Off unless the caller sets it. */
|
|
689
|
+
includeReach?: boolean;
|
|
511
690
|
}
|
|
512
691
|
declare function validateCycloneDx15(doc: unknown): string[];
|
|
513
692
|
declare function renderCycloneDx(input: SbomInput): Record<string, unknown>;
|
|
@@ -528,6 +707,41 @@ declare function buildVex(findings: readonly Finding[], format: VexFormat, meta?
|
|
|
528
707
|
/** Phase 31 worm / VEX / overrides / provenance. Off unless the env flag or CLI option is set. */
|
|
529
708
|
declare function experimentalVex(): boolean;
|
|
530
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
|
+
|
|
531
745
|
/** Static worm indicators: token read + publish / registry PUT / GitHub create / workflow write. */
|
|
532
746
|
declare function analyzeWormIndicators(dir: string): Finding[];
|
|
533
747
|
/** Static: install scripts or code writing .github/, .git/hooks/, or CI config. */
|
|
@@ -544,4 +758,27 @@ declare function provenanceCoverage(packages: readonly {
|
|
|
544
758
|
version: string;
|
|
545
759
|
}[], known: ReadonlyMap<string, boolean>): ProvenanceScore;
|
|
546
760
|
|
|
547
|
-
|
|
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 };
|