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/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,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
- 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 };