ship-safe 7.0.0 → 9.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.
@@ -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;