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/dist/iac.js CHANGED
@@ -1,132 +1,2 @@
1
- import { existsSync, statSync, readFileSync, readdirSync } from 'fs';
2
- import { relative, join } from 'path';
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};
@@ -1,13 +1,2 @@
1
- import { spawnSync } from 'child_process';
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
- /** Deterministic (default) vs AI-added. AI findings never fail CI. */
51
- source?: 'deterministic' | 'ai';
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
- /** Human-readable report: findings grouped by severity, highest first. */
447
- declare function renderConsoleReport(input: ScanResult): string;
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): Promise<Record<string, unknown>>;
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
- 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, MODULE_NAMES, type McpScanOptions, type ModuleContext, type ModuleName, type NotifyConfig, POLICY_FILE_NAME, POLICY_FLAG, POLICY_PEER_ERROR, PolicyError, type PolicyException, type PolicyScanResult, type ResolvedConfig, SEVERITIES, type SbomComponent, type SbomInput, type ScanDiff, type ScanMetadata, type ScanModule, type ScanOptions, type ScanResult, type Severity, type WalkOptions, analyzeWorkflowTampering, analyzeWormIndicators, applyBaseline, applyInlineSuppressions, applyPolicy, buildCycloneDxVex, buildOpenVex, buildSbom, buildVex, builtinModules, createAiClient, diffScans, experimentalVex, failsCi, guardTools, isInternalAi, isLiveAi, isModuleName, isPolicyEnabled, isSeverity, loadConfig, loadPolicy, makeFindingId, mcpCfgEnabled, mcpHomeConfigPaths, meetsThreshold, packageVersion, parseIgnore, parsePolicy, parseScanDocument, provenanceCoverage, readBaseline, renderConsoleReport, renderCycloneDx, renderDiffText, renderGithubAnnotations, renderGitlabCodeQuality, renderHtmlReport, renderJsonReport, renderJunitReport, renderMarkdownReport, renderPrSummary, renderSarifReport, ruleDocsUrl, runAiFix, runFix, runScan, scan, scanMcpConfig, scanTextForSecrets, severityRank, sortFindings, summarize, validateCycloneDx15, validateCycloneDxVex, validateOpenVex, validateSarif21, walkFiles, writeBaseline };
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 };