ship-safe 7.0.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 -21
- package/cli/agents/agent-attestation-agent.js +318 -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/hermes-security-agent.js +536 -0
- package/cli/agents/index.js +63 -22
- package/cli/agents/managed-agent-scanner.js +333 -0
- package/cli/agents/supply-chain-agent.js +1 -1
- package/cli/bin/ship-safe.js +125 -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/red-team.js +2 -2
- package/cli/commands/scan-mcp.js +78 -0
- package/cli/commands/scan-skill.js +248 -5
- package/cli/index.js +5 -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
|
@@ -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;
|