ship-safe 4.1.0 → 4.2.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/cli/__tests__/agents.test.js +496 -0
- package/cli/agents/api-fuzzer.js +234 -224
- package/cli/agents/auth-bypass-agent.js +348 -326
- package/cli/agents/cicd-scanner.js +201 -200
- package/cli/agents/config-auditor.js +458 -413
- package/cli/agents/git-history-scanner.js +170 -167
- package/cli/agents/injection-tester.js +455 -401
- package/cli/agents/llm-redteam.js +251 -251
- package/cli/agents/mobile-scanner.js +225 -225
- package/cli/agents/orchestrator.js +220 -157
- package/cli/agents/scoring-engine.js +225 -207
- package/cli/bin/ship-safe.js +14 -0
- package/cli/commands/audit.js +849 -620
- package/cli/commands/doctor.js +149 -0
- package/cli/commands/remediate.js +7 -3
- package/cli/index.js +56 -53
- package/cli/providers/llm-provider.js +287 -288
- package/cli/utils/cache-manager.js +311 -258
- package/package.json +2 -2
|
@@ -1,200 +1,201 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CICDScanner Agent
|
|
3
|
-
* ==================
|
|
4
|
-
*
|
|
5
|
-
* Detect security issues in CI/CD pipeline configurations.
|
|
6
|
-
* Based on OWASP Top 10 CI/CD Security Risks.
|
|
7
|
-
*
|
|
8
|
-
* Scans: GitHub Actions, GitLab CI, Jenkins, CircleCI,
|
|
9
|
-
* Bitbucket Pipelines, Azure DevOps.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import path from 'path';
|
|
13
|
-
import { BaseAgent } from './base-agent.js';
|
|
14
|
-
|
|
15
|
-
const PATTERNS = [
|
|
16
|
-
// ── CICD-SEC-4: Poisoned Pipeline Execution ────────────────────────────────
|
|
17
|
-
{
|
|
18
|
-
rule: 'CICD_PR_TARGET_CHECKOUT',
|
|
19
|
-
title: 'CI/CD: pull_request_target with PR Checkout',
|
|
20
|
-
regex:
|
|
21
|
-
severity: 'critical',
|
|
22
|
-
cwe: 'CWE-94',
|
|
23
|
-
owasp: 'CICD-SEC-4',
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
basename === '
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
basename === '
|
|
186
|
-
basename === '.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
1
|
+
/**
|
|
2
|
+
* CICDScanner Agent
|
|
3
|
+
* ==================
|
|
4
|
+
*
|
|
5
|
+
* Detect security issues in CI/CD pipeline configurations.
|
|
6
|
+
* Based on OWASP Top 10 CI/CD Security Risks.
|
|
7
|
+
*
|
|
8
|
+
* Scans: GitHub Actions, GitLab CI, Jenkins, CircleCI,
|
|
9
|
+
* Bitbucket Pipelines, Azure DevOps.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { BaseAgent } from './base-agent.js';
|
|
14
|
+
|
|
15
|
+
const PATTERNS = [
|
|
16
|
+
// ── CICD-SEC-4: Poisoned Pipeline Execution ────────────────────────────────
|
|
17
|
+
{
|
|
18
|
+
rule: 'CICD_PR_TARGET_CHECKOUT',
|
|
19
|
+
title: 'CI/CD: pull_request_target with PR Checkout',
|
|
20
|
+
regex: /\bpull_request_target\b/g,
|
|
21
|
+
severity: 'critical',
|
|
22
|
+
cwe: 'CWE-94',
|
|
23
|
+
owasp: 'CICD-SEC-4',
|
|
24
|
+
confidence: 'medium',
|
|
25
|
+
description: 'pull_request_target trigger detected. Runs with base branch privileges and can be exploited if combined with PR checkout.',
|
|
26
|
+
fix: 'Use pull_request trigger instead, or never checkout PR branch in pull_request_target',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
rule: 'CICD_WORKFLOW_RUN',
|
|
30
|
+
title: 'CI/CD: Unrestricted workflow_run Trigger',
|
|
31
|
+
regex: /workflow_run[\s\S]{0,200}types:\s*\[?\s*completed/g,
|
|
32
|
+
severity: 'high',
|
|
33
|
+
cwe: 'CWE-94',
|
|
34
|
+
owasp: 'CICD-SEC-4',
|
|
35
|
+
confidence: 'medium',
|
|
36
|
+
description: 'workflow_run trigger can execute with elevated permissions from a completed workflow.',
|
|
37
|
+
fix: 'Add conditions to check the source workflow and event',
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// ── CICD-SEC-2: Inadequate Identity and Access Management ──────────────────
|
|
41
|
+
{
|
|
42
|
+
rule: 'CICD_EXCESSIVE_PERMISSIONS',
|
|
43
|
+
title: 'CI/CD: Write-All Permissions',
|
|
44
|
+
regex: /permissions\s*:\s*write-all/g,
|
|
45
|
+
severity: 'high',
|
|
46
|
+
cwe: 'CWE-250',
|
|
47
|
+
owasp: 'CICD-SEC-2',
|
|
48
|
+
description: 'Workflow has write-all permissions. Apply least privilege.',
|
|
49
|
+
fix: 'Set granular permissions: permissions: { contents: read, pull-requests: write }',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
rule: 'CICD_PERMISSIVE_TOKEN',
|
|
53
|
+
title: 'CI/CD: Permissive GITHUB_TOKEN',
|
|
54
|
+
regex: /permissions\s*:\s*\n\s*contents\s*:\s*write/g,
|
|
55
|
+
severity: 'medium',
|
|
56
|
+
cwe: 'CWE-250',
|
|
57
|
+
owasp: 'CICD-SEC-2',
|
|
58
|
+
confidence: 'low',
|
|
59
|
+
description: 'GITHUB_TOKEN has contents: write. Consider if read is sufficient.',
|
|
60
|
+
fix: 'Use contents: read unless the workflow needs to push commits',
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// ── CICD-SEC-6: Insufficient Credential Hygiene ────────────────────────────
|
|
64
|
+
{
|
|
65
|
+
rule: 'CICD_SECRET_IN_LOG',
|
|
66
|
+
title: 'CI/CD: Secret Potentially Logged',
|
|
67
|
+
regex: /echo\s+\$\{\{\s*secrets\./g,
|
|
68
|
+
severity: 'critical',
|
|
69
|
+
cwe: 'CWE-532',
|
|
70
|
+
owasp: 'CICD-SEC-6',
|
|
71
|
+
description: 'Secret printed via echo may appear in CI logs. GitHub masks known secrets but not all.',
|
|
72
|
+
fix: 'Never echo secrets. Use them only in environment variables or file writes.',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
rule: 'CICD_HARDCODED_SECRET',
|
|
76
|
+
title: 'CI/CD: Hardcoded Secret in Workflow',
|
|
77
|
+
regex: /(?:api[_-]?key|token|password|secret)\s*[:=]\s*["'][a-zA-Z0-9_\-]{20,}["']/gi,
|
|
78
|
+
severity: 'critical',
|
|
79
|
+
cwe: 'CWE-798',
|
|
80
|
+
owasp: 'CICD-SEC-6',
|
|
81
|
+
description: 'Hardcoded secret in CI/CD configuration. Use repository/organization secrets.',
|
|
82
|
+
fix: 'Move to GitHub/GitLab secrets: ${{ secrets.MY_SECRET }}',
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// ── CICD-SEC-8: Ungoverned Usage of 3rd Party Services ────────────────────
|
|
86
|
+
{
|
|
87
|
+
rule: 'CICD_UNPINNED_ACTION',
|
|
88
|
+
title: 'CI/CD: Unpinned GitHub Action (uses @main/master)',
|
|
89
|
+
regex: /uses\s*:\s*[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+@(?:main|master|latest|v\d+)\b/g,
|
|
90
|
+
severity: 'high',
|
|
91
|
+
cwe: 'CWE-829',
|
|
92
|
+
owasp: 'CICD-SEC-8',
|
|
93
|
+
description: 'GitHub Action pinned to mutable tag. Pin to a specific commit SHA.',
|
|
94
|
+
fix: 'Pin to commit SHA: uses: actions/checkout@abcdef1234567890 # v4.1.0',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
rule: 'CICD_UNVERIFIED_ACTION',
|
|
98
|
+
title: 'CI/CD: Unverified Third-Party Action',
|
|
99
|
+
regex: /uses\s*:\s*(?!actions\/|github\/|docker\/)[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+@/g,
|
|
100
|
+
severity: 'medium',
|
|
101
|
+
cwe: 'CWE-829',
|
|
102
|
+
owasp: 'CICD-SEC-8',
|
|
103
|
+
confidence: 'low',
|
|
104
|
+
description: 'Third-party GitHub Action not from verified publisher. Review source code.',
|
|
105
|
+
fix: 'Pin to commit SHA, review the action source, or use an official alternative',
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// ── CICD-SEC-3: Dependency Chain Abuse ─────────────────────────────────────
|
|
109
|
+
{
|
|
110
|
+
rule: 'CICD_NO_LOCKFILE_INSTALL',
|
|
111
|
+
title: 'CI/CD: Install Without Lockfile',
|
|
112
|
+
regex: /npm\s+install(?!\s+--(?:frozen|ci))|yarn\s+(?!--frozen-lockfile)/g,
|
|
113
|
+
severity: 'high',
|
|
114
|
+
cwe: 'CWE-829',
|
|
115
|
+
owasp: 'CICD-SEC-3',
|
|
116
|
+
confidence: 'medium',
|
|
117
|
+
description: 'CI runs npm install without --frozen-lockfile. Builds are non-deterministic.',
|
|
118
|
+
fix: 'Use npm ci (not npm install) or yarn --frozen-lockfile in CI',
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// ── CICD-SEC-7: Insecure System Configuration ─────────────────────────────
|
|
122
|
+
{
|
|
123
|
+
rule: 'CICD_SELF_HOSTED_RUNNER',
|
|
124
|
+
title: 'CI/CD: Self-Hosted Runner',
|
|
125
|
+
regex: /runs-on\s*:\s*self-hosted/g,
|
|
126
|
+
severity: 'medium',
|
|
127
|
+
cwe: 'CWE-250',
|
|
128
|
+
owasp: 'CICD-SEC-7',
|
|
129
|
+
confidence: 'medium',
|
|
130
|
+
description: 'Self-hosted runners may persist state between jobs. Use ephemeral runners.',
|
|
131
|
+
fix: 'Use ephemeral self-hosted runners that are cleaned after each job',
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// ── CICD-SEC-9: Improper Artifact Integrity Validation ────────────────────
|
|
135
|
+
{
|
|
136
|
+
rule: 'CICD_NO_ARTIFACT_VERIFY',
|
|
137
|
+
title: 'CI/CD: Artifact Used Without Verification',
|
|
138
|
+
regex: /download-artifact|cache@|restore-keys/g,
|
|
139
|
+
severity: 'low',
|
|
140
|
+
cwe: 'CWE-345',
|
|
141
|
+
owasp: 'CICD-SEC-9',
|
|
142
|
+
confidence: 'low',
|
|
143
|
+
description: 'Artifacts/caches used without integrity verification. Consider adding checksums.',
|
|
144
|
+
fix: 'Verify artifact integrity with checksums or signatures',
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// ── General CI/CD Issues ───────────────────────────────────────────────────
|
|
148
|
+
{
|
|
149
|
+
rule: 'CICD_CURL_PIPE_BASH',
|
|
150
|
+
title: 'CI/CD: curl | bash Anti-Pattern',
|
|
151
|
+
regex: /curl\s+[^|]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh)/g,
|
|
152
|
+
severity: 'critical',
|
|
153
|
+
cwe: 'CWE-829',
|
|
154
|
+
description: 'Piping curl to shell is dangerous. Download, verify, then execute.',
|
|
155
|
+
fix: 'Download file first, verify checksum, then execute: curl -o script.sh && sha256sum -c && bash script.sh',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
rule: 'CICD_SCRIPT_INJECTION',
|
|
159
|
+
title: 'CI/CD: Script Injection via Expressions',
|
|
160
|
+
regex: /run\s*:\s*[^\n]*\$\{\{\s*(?:github\.event\.(?:issue|comment|pull_request|review)\.(?:title|body|head\.ref|label))/g,
|
|
161
|
+
severity: 'critical',
|
|
162
|
+
cwe: 'CWE-78',
|
|
163
|
+
owasp: 'CICD-SEC-4',
|
|
164
|
+
description: 'GitHub expression in run step. Attacker-controlled values can inject shell commands.',
|
|
165
|
+
fix: 'Use environment variables: env: TITLE: ${{ github.event.issue.title }} then run: echo "$TITLE"',
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
export class CICDScanner extends BaseAgent {
|
|
170
|
+
constructor() {
|
|
171
|
+
super('CICDScanner', 'Detect CI/CD pipeline security issues (OWASP CI/CD Top 10)', 'cicd');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async analyze(context) {
|
|
175
|
+
const { rootPath, files } = context;
|
|
176
|
+
|
|
177
|
+
const ciFiles = files.filter(f => {
|
|
178
|
+
const relPath = path.relative(rootPath, f).replace(/\\/g, '/');
|
|
179
|
+
const basename = path.basename(f);
|
|
180
|
+
return (
|
|
181
|
+
relPath.startsWith('.github/workflows/') ||
|
|
182
|
+
basename === '.gitlab-ci.yml' ||
|
|
183
|
+
basename === 'Jenkinsfile' ||
|
|
184
|
+
relPath.startsWith('.circleci/') ||
|
|
185
|
+
basename === 'bitbucket-pipelines.yml' ||
|
|
186
|
+
basename === 'azure-pipelines.yml' ||
|
|
187
|
+
basename === '.travis.yml'
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (ciFiles.length === 0) return [];
|
|
192
|
+
|
|
193
|
+
let findings = [];
|
|
194
|
+
for (const file of ciFiles) {
|
|
195
|
+
findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
|
|
196
|
+
}
|
|
197
|
+
return findings;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export default CICDScanner;
|