ship-safe 6.4.0 → 8.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/README.md +80 -23
- package/cli/agents/agent-attestation-agent.js +318 -0
- package/cli/agents/agent-config-scanner.js +15 -0
- package/cli/agents/agentic-security-agent.js +35 -0
- package/cli/agents/cicd-scanner.js +22 -0
- package/cli/agents/config-auditor.js +235 -0
- package/cli/agents/deep-analyzer.js +39 -19
- package/cli/agents/hermes-security-agent.js +536 -0
- package/cli/agents/index.js +65 -21
- package/cli/agents/managed-agent-scanner.js +333 -0
- package/cli/agents/memory-poisoning-agent.js +304 -0
- package/cli/agents/scoring-engine.js +16 -1
- package/cli/agents/supply-chain-agent.js +129 -3
- package/cli/bin/ship-safe.js +178 -5
- package/cli/commands/audit.js +116 -2
- package/cli/commands/autofix.js +383 -0
- package/cli/commands/env-audit.js +349 -0
- package/cli/commands/live-advisories.js +241 -0
- package/cli/commands/red-team.js +2 -2
- package/cli/commands/scan-mcp.js +78 -0
- package/cli/commands/scan-skill.js +248 -5
- package/cli/commands/watch.js +205 -0
- package/cli/index.js +5 -0
- package/cli/providers/llm-provider.js +89 -1
- package/cli/utils/compliance-map.js +66 -0
- package/cli/utils/hermes-tool-registry.js +252 -0
- package/cli/utils/patterns.js +1 -0
- package/cli/utils/plugin-loader.js +276 -0
- package/cli/utils/scan-playbook.js +312 -0
- package/cli/utils/security-memory.js +296 -0
- package/package.json +2 -2
|
@@ -241,6 +241,28 @@ const PATTERNS = [
|
|
|
241
241
|
description: 'claw-code (Rust/Python Claude Code rewrite) is invoked with --dangerously-skip-permissions in CI. Any prompt injection in the workspace executes without confirmation.',
|
|
242
242
|
fix: 'Remove --dangerously-skip-permissions. Use --permission-mode=workspace-write for CI automation.',
|
|
243
243
|
},
|
|
244
|
+
|
|
245
|
+
// ── Branch Name Injection (Codex-class attack, CVE pending) ──────────────
|
|
246
|
+
{
|
|
247
|
+
rule: 'CICD_BRANCH_NAME_INJECTION',
|
|
248
|
+
title: 'CI/CD: Unsanitized Branch Name in Shell Command',
|
|
249
|
+
regex: /(?:git\s+(?:checkout|switch|clone\s+--branch|fetch\s+origin))\s+(?:\$\{\{[^}]*(?:head\.ref|branch|ref_name)[^}]*\}\}|\$(?:BRANCH|GITHUB_HEAD_REF|CI_COMMIT_BRANCH|BITBUCKET_BRANCH))/gi,
|
|
250
|
+
severity: 'critical',
|
|
251
|
+
cwe: 'CWE-78',
|
|
252
|
+
owasp: 'CICD-SEC-4',
|
|
253
|
+
description: 'Branch name from an external source (PR head ref, environment variable) is passed directly to a git shell command without sanitization. Attackers can create branches with names containing shell metacharacters to inject arbitrary commands. This is the exact attack vector used in the OpenAI Codex GitHub token theft (BeyondTrust Phantom Labs, Mar 2026).',
|
|
254
|
+
fix: 'Sanitize branch names: strip shell metacharacters, use -- to separate git options from arguments, or use actions/checkout which handles this safely.',
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
rule: 'CICD_BRANCH_NAME_IN_RUN',
|
|
258
|
+
title: 'CI/CD: Branch Name Interpolated in run Step',
|
|
259
|
+
regex: /run\s*:\s*[^\n]*\$\{\{\s*(?:github\.head_ref|github\.ref_name)\s*\}\}/g,
|
|
260
|
+
severity: 'high',
|
|
261
|
+
cwe: 'CWE-78',
|
|
262
|
+
owasp: 'CICD-SEC-4',
|
|
263
|
+
description: 'GitHub expression for branch name used directly in a run step. An attacker can craft a branch name with shell injection payloads. This pattern was exploited in the OpenAI Codex vulnerability to steal GitHub OAuth tokens.',
|
|
264
|
+
fix: 'Assign to an environment variable first: env: BRANCH: ${{ github.head_ref }}, then reference as "$BRANCH" (quoted) in the run step.',
|
|
265
|
+
},
|
|
244
266
|
];
|
|
245
267
|
|
|
246
268
|
export class CICDScanner extends BaseAgent {
|
|
@@ -299,6 +299,74 @@ const CONFIG_PATTERNS = [
|
|
|
299
299
|
fix: 'Remove sensitive host mounts. Use named volumes for data persistence.',
|
|
300
300
|
},
|
|
301
301
|
|
|
302
|
+
// ── Container Sandbox Hardening (Mythos-class escape prevention) ──────────
|
|
303
|
+
{
|
|
304
|
+
rule: 'DOCKER_NO_READ_ONLY_ROOT',
|
|
305
|
+
title: 'Docker: Writable Root Filesystem',
|
|
306
|
+
regex: /(?:read_only|readOnlyRootFilesystem)\s*:\s*false/g,
|
|
307
|
+
severity: 'high',
|
|
308
|
+
cwe: 'CWE-732',
|
|
309
|
+
description: 'Container has writable root filesystem. Attackers can write exploit payloads after gaining code execution.',
|
|
310
|
+
fix: 'Set read_only: true (Compose) or readOnlyRootFilesystem: true (K8s). Use tmpfs mounts for writable paths.',
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
rule: 'DOCKER_NO_NEW_PRIVILEGES',
|
|
314
|
+
title: 'Docker: Missing no-new-privileges Flag',
|
|
315
|
+
regex: /security_opt\s*:\s*\[?[^\]]*no-new-privileges\s*:\s*false/g,
|
|
316
|
+
severity: 'high',
|
|
317
|
+
cwe: 'CWE-269',
|
|
318
|
+
description: 'Container allows privilege escalation via setuid/setgid binaries. This enables sandbox escape.',
|
|
319
|
+
fix: 'Add security_opt: [no-new-privileges:true] to container configuration.',
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
rule: 'DOCKER_NETWORK_HOST',
|
|
323
|
+
title: 'Docker: Host Network Mode',
|
|
324
|
+
regex: /network_mode\s*:\s*["']?host["']?/g,
|
|
325
|
+
severity: 'critical',
|
|
326
|
+
cwe: 'CWE-284',
|
|
327
|
+
description: 'Container uses host network stack. A sandbox escape gains full network access to the host and local network.',
|
|
328
|
+
fix: 'Remove network_mode: host. Use bridge networking with explicit port mappings.',
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
rule: 'DOCKER_CAP_SYS_ADMIN',
|
|
332
|
+
title: 'Docker: SYS_ADMIN Capability',
|
|
333
|
+
regex: /cap_add\s*:[\s\S]*?(?:SYS_ADMIN|ALL)/g,
|
|
334
|
+
severity: 'critical',
|
|
335
|
+
cwe: 'CWE-250',
|
|
336
|
+
description: 'SYS_ADMIN or ALL capabilities grant near-root host access. This is equivalent to privileged mode.',
|
|
337
|
+
fix: 'Remove SYS_ADMIN from cap_add. Use the minimum capabilities required.',
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
rule: 'DOCKER_PID_HOST',
|
|
341
|
+
title: 'Docker: Host PID Namespace',
|
|
342
|
+
regex: /pid\s*:\s*["']?host["']?/g,
|
|
343
|
+
severity: 'high',
|
|
344
|
+
cwe: 'CWE-284',
|
|
345
|
+
description: 'Container shares host PID namespace. Processes can inspect and signal host processes after escape.',
|
|
346
|
+
fix: 'Remove pid: host. Use default PID namespace isolation.',
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// ── Kubernetes Sandbox Hardening ──────────────────────────────────────────
|
|
350
|
+
{
|
|
351
|
+
rule: 'K8S_ALLOW_PRIVILEGE_ESCALATION',
|
|
352
|
+
title: 'Kubernetes: allowPrivilegeEscalation Not Disabled',
|
|
353
|
+
regex: /allowPrivilegeEscalation\s*:\s*true/g,
|
|
354
|
+
severity: 'high',
|
|
355
|
+
cwe: 'CWE-269',
|
|
356
|
+
description: 'Container allows privilege escalation. Set to false to prevent setuid-based escape.',
|
|
357
|
+
fix: 'Set allowPrivilegeEscalation: false in securityContext.',
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
rule: 'K8S_NO_SECCOMP',
|
|
361
|
+
title: 'Kubernetes: Missing Seccomp Profile',
|
|
362
|
+
regex: /securityContext\s*:\s*\{(?:(?!seccompProfile)[\s\S])*?\}/g,
|
|
363
|
+
severity: 'medium',
|
|
364
|
+
cwe: 'CWE-693',
|
|
365
|
+
confidence: 'low',
|
|
366
|
+
description: 'No seccomp profile configured. Seccomp restricts syscalls available to the container, reducing escape surface.',
|
|
367
|
+
fix: 'Add seccompProfile: { type: RuntimeDefault } to securityContext.',
|
|
368
|
+
},
|
|
369
|
+
|
|
302
370
|
// ── Nginx ──────────────────────────────────────────────────────────────────
|
|
303
371
|
{
|
|
304
372
|
rule: 'NGINX_AUTOINDEX',
|
|
@@ -416,12 +484,14 @@ export class ConfigAuditor extends BaseAgent {
|
|
|
416
484
|
for (const file of dockerfiles) {
|
|
417
485
|
findings = findings.concat(this.scanFileWithPatterns(file, DOCKERFILE_PATTERNS));
|
|
418
486
|
findings = findings.concat(this.checkDockerfileUser(file));
|
|
487
|
+
findings = findings.concat(this.checkDockerEngineVersion(file));
|
|
419
488
|
}
|
|
420
489
|
|
|
421
490
|
// ── Scan docker-compose ───────────────────────────────────────────────────
|
|
422
491
|
const composeFiles = files.filter(f => /docker-compose\.ya?ml$/i.test(path.basename(f)));
|
|
423
492
|
for (const file of composeFiles) {
|
|
424
493
|
findings = findings.concat(this.scanFileWithPatterns(file, CONFIG_PATTERNS));
|
|
494
|
+
findings = findings.concat(this.checkComposeNetworkIsolation(file));
|
|
425
495
|
}
|
|
426
496
|
|
|
427
497
|
// ── Scan Terraform ────────────────────────────────────────────────────────
|
|
@@ -430,6 +500,11 @@ export class ConfigAuditor extends BaseAgent {
|
|
|
430
500
|
findings = findings.concat(this.scanFileWithPatterns(file, CONFIG_PATTERNS));
|
|
431
501
|
}
|
|
432
502
|
|
|
503
|
+
// ── Scan for S3 Files NFS mounts (Terraform, ECS task defs, K8s) ─────────
|
|
504
|
+
for (const file of tfFiles) {
|
|
505
|
+
findings = findings.concat(this.checkS3FilesMountSecurity(file));
|
|
506
|
+
}
|
|
507
|
+
|
|
433
508
|
// ── Scan Kubernetes manifests ─────────────────────────────────────────────
|
|
434
509
|
const k8sFiles = files.filter(f => {
|
|
435
510
|
const relPath = path.relative(rootPath, f).replace(/\\/g, '/');
|
|
@@ -439,6 +514,20 @@ export class ConfigAuditor extends BaseAgent {
|
|
|
439
514
|
findings = findings.concat(this.scanFileWithPatterns(file, CONFIG_PATTERNS));
|
|
440
515
|
}
|
|
441
516
|
|
|
517
|
+
// ── S3 Files NFS mounts in K8s ─────────────────────────────────────────────
|
|
518
|
+
for (const file of k8sFiles) {
|
|
519
|
+
findings = findings.concat(this.checkS3FilesMountSecurity(file));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ── ECS task definitions with S3 Files ────────────────────────────────────
|
|
523
|
+
const ecsFiles = files.filter(f => {
|
|
524
|
+
const content = this.readFile(f);
|
|
525
|
+
return content && /taskDefinition|containerDefinitions/i.test(content) && /\.(?:json|ya?ml|tf)$/i.test(f);
|
|
526
|
+
});
|
|
527
|
+
for (const file of ecsFiles) {
|
|
528
|
+
findings = findings.concat(this.checkS3FilesMountSecurity(file));
|
|
529
|
+
}
|
|
530
|
+
|
|
442
531
|
// ── Project-level: K8s NetworkPolicy check ─────────────────────────────────
|
|
443
532
|
if (k8sFiles.length > 0) {
|
|
444
533
|
const hasNetworkPolicy = k8sFiles.some(f => {
|
|
@@ -516,6 +605,152 @@ export class ConfigAuditor extends BaseAgent {
|
|
|
516
605
|
}
|
|
517
606
|
return [];
|
|
518
607
|
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Check Dockerfile for vulnerable Docker Engine versions (CVE-2026-34040).
|
|
611
|
+
* Detects version pins in FROM directives and docker-compose engine constraints.
|
|
612
|
+
*/
|
|
613
|
+
checkDockerEngineVersion(filePath) {
|
|
614
|
+
const content = this.readFile(filePath);
|
|
615
|
+
if (!content) return [];
|
|
616
|
+
|
|
617
|
+
const findings = [];
|
|
618
|
+
const lines = content.split('\n');
|
|
619
|
+
|
|
620
|
+
for (let i = 0; i < lines.length; i++) {
|
|
621
|
+
const line = lines[i];
|
|
622
|
+
if (this.isSuppressed(line)) continue;
|
|
623
|
+
|
|
624
|
+
// Match: FROM docker:<version> or engine version references in comments/labels
|
|
625
|
+
const versionMatch = line.match(/(?:docker|moby)[:\s]+(\d+)\.(\d+)\.(\d+)/i);
|
|
626
|
+
if (versionMatch) {
|
|
627
|
+
const major = parseInt(versionMatch[1], 10);
|
|
628
|
+
const minor = parseInt(versionMatch[2], 10);
|
|
629
|
+
const patch = parseInt(versionMatch[3], 10);
|
|
630
|
+
|
|
631
|
+
// CVE-2026-34040: fixed in 29.3.1
|
|
632
|
+
if (major < 29 || (major === 29 && minor < 3) || (major === 29 && minor === 3 && patch < 1)) {
|
|
633
|
+
findings.push(createFinding({
|
|
634
|
+
file: filePath,
|
|
635
|
+
line: i + 1,
|
|
636
|
+
severity: 'critical',
|
|
637
|
+
category: 'config',
|
|
638
|
+
rule: 'DOCKER_CVE_2026_34040',
|
|
639
|
+
title: 'Docker: Engine Version Vulnerable to AuthZ Bypass (CVE-2026-34040)',
|
|
640
|
+
description: `Docker Engine ${versionMatch[1]}.${versionMatch[2]}.${versionMatch[3]} is vulnerable to CVE-2026-34040 (CVSS 8.8). Attackers can bypass authorization plugins via oversized requests, creating privileged containers and extracting host credentials.`,
|
|
641
|
+
matched: versionMatch[0],
|
|
642
|
+
cwe: 'CWE-863',
|
|
643
|
+
fix: 'Upgrade to Docker Engine 29.3.1 or later. As a temporary mitigation, run Docker in rootless mode or use --userns-remap.',
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return findings;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Check for S3 Files NFS mounts without security restrictions.
|
|
653
|
+
* S3 Files (April 2026) allows mounting S3 buckets as NFS filesystems.
|
|
654
|
+
* When AI agents have filesystem access to mounted S3 buckets, prompt injection
|
|
655
|
+
* can access entire buckets through normal file reads.
|
|
656
|
+
*/
|
|
657
|
+
checkS3FilesMountSecurity(filePath) {
|
|
658
|
+
const content = this.readFile(filePath);
|
|
659
|
+
if (!content) return [];
|
|
660
|
+
|
|
661
|
+
const findings = [];
|
|
662
|
+
const lines = content.split('\n');
|
|
663
|
+
|
|
664
|
+
for (let i = 0; i < lines.length; i++) {
|
|
665
|
+
const line = lines[i];
|
|
666
|
+
if (this.isSuppressed(line)) continue;
|
|
667
|
+
|
|
668
|
+
// Detect S3 Files / S3 NFS mount references
|
|
669
|
+
const isS3Mount = /(?:s3[_-]?files|s3.*(?:nfs|mount|filesystem)|file_system_id.*s3|efs.*s3|mount.*s3:\/\/)/i.test(line);
|
|
670
|
+
if (!isS3Mount) continue;
|
|
671
|
+
|
|
672
|
+
// Check surrounding context (10 lines) for read-only or prefix scoping
|
|
673
|
+
const contextStart = Math.max(0, i - 5);
|
|
674
|
+
const contextEnd = Math.min(lines.length, i + 10);
|
|
675
|
+
const context = lines.slice(contextStart, contextEnd).join('\n');
|
|
676
|
+
|
|
677
|
+
const hasReadOnly = /read[_-]?only|readOnly|ro\b|access_mode.*read/i.test(context);
|
|
678
|
+
const hasPrefixScope = /prefix|sub[_-]?path|path[_-]?prefix|mount[_-]?point.*\/[a-z]/i.test(context);
|
|
679
|
+
|
|
680
|
+
if (!hasReadOnly) {
|
|
681
|
+
findings.push(createFinding({
|
|
682
|
+
file: filePath,
|
|
683
|
+
line: i + 1,
|
|
684
|
+
severity: 'high',
|
|
685
|
+
category: 'config',
|
|
686
|
+
rule: 'S3_FILES_MOUNT_NOT_READONLY',
|
|
687
|
+
title: 'S3 Files: NFS Mount Without Read-Only Restriction',
|
|
688
|
+
description: 'S3 bucket mounted as a filesystem without read-only flag. AI agents or compromised workloads can write arbitrary data to the entire bucket via standard file operations.',
|
|
689
|
+
matched: line.trim(),
|
|
690
|
+
cwe: 'CWE-732',
|
|
691
|
+
fix: 'Mount S3 Files with read-only access unless writes are explicitly needed. Use IAM policies to restrict access scope.',
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (!hasPrefixScope) {
|
|
696
|
+
findings.push(createFinding({
|
|
697
|
+
file: filePath,
|
|
698
|
+
line: i + 1,
|
|
699
|
+
severity: 'medium',
|
|
700
|
+
category: 'config',
|
|
701
|
+
rule: 'S3_FILES_MOUNT_NO_PREFIX',
|
|
702
|
+
title: 'S3 Files: NFS Mount Without Prefix Scoping',
|
|
703
|
+
description: 'S3 bucket mounted at root without prefix scoping. The entire bucket is accessible as a filesystem. Scope mounts to specific prefixes to limit blast radius.',
|
|
704
|
+
matched: line.trim(),
|
|
705
|
+
cwe: 'CWE-284',
|
|
706
|
+
confidence: 'medium',
|
|
707
|
+
fix: 'Mount only the specific S3 prefix needed (e.g., s3://bucket/app-data/) instead of the entire bucket.',
|
|
708
|
+
}));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return findings;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Check if a docker-compose service running an AI agent has network restrictions.
|
|
716
|
+
* Services with "agent", "ai", "llm", "mcp" in the name or image without
|
|
717
|
+
* explicit network_mode or networks configuration are flagged.
|
|
718
|
+
*/
|
|
719
|
+
checkComposeNetworkIsolation(filePath) {
|
|
720
|
+
const content = this.readFile(filePath);
|
|
721
|
+
if (!content) return [];
|
|
722
|
+
|
|
723
|
+
const findings = [];
|
|
724
|
+
// Check for services that look like AI/agent containers without network restrictions
|
|
725
|
+
const serviceBlockRe = /^\s{2}(\S+):\s*\n((?:\s{4,}.+\n)*)/gm;
|
|
726
|
+
let match;
|
|
727
|
+
while ((match = serviceBlockRe.exec(content)) !== null) {
|
|
728
|
+
const serviceName = match[1];
|
|
729
|
+
const serviceBlock = match[2];
|
|
730
|
+
const isAgentService = /(?:agent|ai|llm|mcp|model|inference)/i.test(serviceName) ||
|
|
731
|
+
/(?:agent|ai|llm|mcp|model|inference)/i.test(serviceBlock);
|
|
732
|
+
|
|
733
|
+
if (isAgentService) {
|
|
734
|
+
const hasNetworkRestriction = /network_mode\s*:|networks\s*:/m.test(serviceBlock);
|
|
735
|
+
if (!hasNetworkRestriction) {
|
|
736
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
737
|
+
findings.push(createFinding({
|
|
738
|
+
file: filePath,
|
|
739
|
+
line: lineNum,
|
|
740
|
+
severity: 'high',
|
|
741
|
+
category: 'config',
|
|
742
|
+
rule: 'COMPOSE_AGENT_UNRESTRICTED_NETWORK',
|
|
743
|
+
title: 'Docker Compose: AI Agent Service Without Network Restrictions',
|
|
744
|
+
description: `Service "${serviceName}" appears to run an AI agent but has no network_mode or networks configuration. Unrestricted outbound network access enables data exfiltration after sandbox escape.`,
|
|
745
|
+
matched: serviceName,
|
|
746
|
+
cwe: 'CWE-284',
|
|
747
|
+
fix: 'Add network_mode: none for fully isolated agents, or configure a restricted network with explicit egress rules.',
|
|
748
|
+
}));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return findings;
|
|
753
|
+
}
|
|
519
754
|
}
|
|
520
755
|
|
|
521
756
|
export default ConfigAuditor;
|
|
@@ -25,8 +25,15 @@ import { createProvider, autoDetectProvider } from '../providers/llm-provider.js
|
|
|
25
25
|
// CONSTANTS
|
|
26
26
|
// =============================================================================
|
|
27
27
|
|
|
28
|
-
/** Max file content
|
|
29
|
-
const
|
|
28
|
+
/** Max file content per finding for standard providers (tokens cost money) */
|
|
29
|
+
const MAX_FILE_CHARS_DEFAULT = 4000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Max file content per finding for large-context providers (Gemma 4 128K–256K).
|
|
33
|
+
* Sending the full file enables cross-function taint tracing that a 40-line
|
|
34
|
+
* window cannot catch.
|
|
35
|
+
*/
|
|
36
|
+
const MAX_FILE_CHARS_LARGE_CTX = 80000;
|
|
30
37
|
|
|
31
38
|
/** Max findings to analyze per run (cost control) */
|
|
32
39
|
const MAX_FINDINGS = 30;
|
|
@@ -84,6 +91,14 @@ export class DeepAnalyzer {
|
|
|
84
91
|
this.verbose = options.verbose || false;
|
|
85
92
|
this.spentCents = 0;
|
|
86
93
|
this.analyzedCount = 0;
|
|
94
|
+
|
|
95
|
+
// If the provider advertises a large context window (Gemma 4, etc.),
|
|
96
|
+
// increase file context and batch size to take full advantage.
|
|
97
|
+
const ctxWindow = this.provider?.contextWindow ?? 0;
|
|
98
|
+
this.largeContext = ctxWindow >= 65536;
|
|
99
|
+
this.maxFileChars = this.largeContext ? MAX_FILE_CHARS_LARGE_CTX : MAX_FILE_CHARS_DEFAULT;
|
|
100
|
+
// Larger batches for local large-context models (no per-token cost)
|
|
101
|
+
this.batchSize = this.largeContext ? 15 : 5;
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
/**
|
|
@@ -91,11 +106,11 @@ export class DeepAnalyzer {
|
|
|
91
106
|
* Returns null if no provider is available.
|
|
92
107
|
*/
|
|
93
108
|
static create(rootPath, options = {}) {
|
|
94
|
-
// --local flag: use Ollama
|
|
109
|
+
// --local flag: use Gemma 4 via Ollama (structured output, large context)
|
|
95
110
|
if (options.local) {
|
|
96
|
-
const provider = createProvider('
|
|
97
|
-
model:
|
|
98
|
-
baseUrl: options.ollamaUrl
|
|
111
|
+
const provider = createProvider('gemma4', null, {
|
|
112
|
+
model: options.model,
|
|
113
|
+
baseUrl: options.ollamaUrl,
|
|
99
114
|
});
|
|
100
115
|
return new DeepAnalyzer({ provider, ...options });
|
|
101
116
|
}
|
|
@@ -141,11 +156,10 @@ export class DeepAnalyzer {
|
|
|
141
156
|
toAnalyze.length = Math.max(1, affordable);
|
|
142
157
|
}
|
|
143
158
|
|
|
144
|
-
// Batch findings
|
|
145
|
-
const batchSize = 5;
|
|
159
|
+
// Batch findings — larger batches for large-context providers (Gemma 4 etc.)
|
|
146
160
|
const results = new Map();
|
|
147
161
|
|
|
148
|
-
for (let i = 0; i < toAnalyze.length; i += batchSize) {
|
|
162
|
+
for (let i = 0; i < toAnalyze.length; i += this.batchSize) {
|
|
149
163
|
// Budget check before each batch
|
|
150
164
|
if (this.spentCents >= this.budgetCents) {
|
|
151
165
|
if (this.verbose) {
|
|
@@ -154,7 +168,7 @@ export class DeepAnalyzer {
|
|
|
154
168
|
break;
|
|
155
169
|
}
|
|
156
170
|
|
|
157
|
-
const batch = toAnalyze.slice(i, i + batchSize);
|
|
171
|
+
const batch = toAnalyze.slice(i, i + this.batchSize);
|
|
158
172
|
const prompt = this._buildPrompt(batch, context);
|
|
159
173
|
|
|
160
174
|
try {
|
|
@@ -261,16 +275,22 @@ ${JSON.stringify(items, null, 2)}`;
|
|
|
261
275
|
const lines = content.split('\n');
|
|
262
276
|
const lineNum = finding.line || 1;
|
|
263
277
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
.map((l, i) => `${
|
|
269
|
-
|
|
278
|
+
let context;
|
|
279
|
+
if (this.largeContext) {
|
|
280
|
+
// Large-context providers (Gemma 4): send the entire file so the model
|
|
281
|
+
// can trace taint flows across functions, not just the immediate window.
|
|
282
|
+
context = lines.map((l, i) => `${i + 1}: ${l}`).join('\n');
|
|
283
|
+
} else {
|
|
284
|
+
// Standard providers: 40-line window around the finding
|
|
285
|
+
const start = Math.max(0, lineNum - 21);
|
|
286
|
+
const end = Math.min(lines.length, lineNum + 20);
|
|
287
|
+
context = lines.slice(start, end)
|
|
288
|
+
.map((l, i) => `${start + i + 1}: ${l}`)
|
|
289
|
+
.join('\n');
|
|
290
|
+
}
|
|
270
291
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
context = context.slice(0, MAX_FILE_CHARS) + '\n... (truncated)';
|
|
292
|
+
if (context.length > this.maxFileChars) {
|
|
293
|
+
context = context.slice(0, this.maxFileChars) + '\n... (truncated)';
|
|
274
294
|
}
|
|
275
295
|
|
|
276
296
|
return context;
|