cipher-security 2.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/bin/cipher.js +566 -0
- package/lib/api/billing.js +321 -0
- package/lib/api/compliance.js +693 -0
- package/lib/api/controls.js +1401 -0
- package/lib/api/index.js +49 -0
- package/lib/api/marketplace.js +467 -0
- package/lib/api/openai-proxy.js +383 -0
- package/lib/api/server.js +685 -0
- package/lib/autonomous/feedback-loop.js +554 -0
- package/lib/autonomous/framework.js +512 -0
- package/lib/autonomous/index.js +97 -0
- package/lib/autonomous/leaderboard.js +594 -0
- package/lib/autonomous/modes/architect.js +412 -0
- package/lib/autonomous/modes/blue.js +386 -0
- package/lib/autonomous/modes/incident.js +684 -0
- package/lib/autonomous/modes/privacy.js +369 -0
- package/lib/autonomous/modes/purple.js +294 -0
- package/lib/autonomous/modes/recon.js +250 -0
- package/lib/autonomous/parallel.js +587 -0
- package/lib/autonomous/researcher.js +583 -0
- package/lib/autonomous/runner.js +955 -0
- package/lib/autonomous/scheduler.js +615 -0
- package/lib/autonomous/task-parser.js +127 -0
- package/lib/autonomous/validators/forensic.js +266 -0
- package/lib/autonomous/validators/osint.js +216 -0
- package/lib/autonomous/validators/privacy.js +296 -0
- package/lib/autonomous/validators/purple.js +298 -0
- package/lib/autonomous/validators/sigma.js +248 -0
- package/lib/autonomous/validators/threat-model.js +363 -0
- package/lib/benchmark/agent.js +119 -0
- package/lib/benchmark/baselines.js +43 -0
- package/lib/benchmark/builder.js +143 -0
- package/lib/benchmark/config.js +35 -0
- package/lib/benchmark/coordinator.js +91 -0
- package/lib/benchmark/index.js +20 -0
- package/lib/benchmark/llm.js +58 -0
- package/lib/benchmark/models.js +137 -0
- package/lib/benchmark/reporter.js +103 -0
- package/lib/benchmark/runner.js +103 -0
- package/lib/benchmark/sandbox.js +96 -0
- package/lib/benchmark/scorer.js +32 -0
- package/lib/benchmark/solver.js +166 -0
- package/lib/benchmark/tools.js +62 -0
- package/lib/bot/bot.js +238 -0
- package/lib/brand.js +105 -0
- package/lib/commands.js +100 -0
- package/lib/complexity.js +377 -0
- package/lib/config.js +213 -0
- package/lib/gateway/client.js +309 -0
- package/lib/gateway/commands.js +991 -0
- package/lib/gateway/config-validate.js +109 -0
- package/lib/gateway/gateway.js +367 -0
- package/lib/gateway/index.js +62 -0
- package/lib/gateway/mode.js +309 -0
- package/lib/gateway/plugins.js +222 -0
- package/lib/gateway/prompt.js +214 -0
- package/lib/mcp/server.js +262 -0
- package/lib/memory/compressor.js +425 -0
- package/lib/memory/engine.js +763 -0
- package/lib/memory/evolution.js +668 -0
- package/lib/memory/index.js +58 -0
- package/lib/memory/orchestrator.js +506 -0
- package/lib/memory/retriever.js +515 -0
- package/lib/memory/synthesizer.js +333 -0
- package/lib/pipeline/async-scanner.js +510 -0
- package/lib/pipeline/binary-analysis.js +1043 -0
- package/lib/pipeline/dom-xss-scanner.js +435 -0
- package/lib/pipeline/github-actions.js +792 -0
- package/lib/pipeline/index.js +124 -0
- package/lib/pipeline/osint.js +498 -0
- package/lib/pipeline/sarif.js +373 -0
- package/lib/pipeline/scanner.js +880 -0
- package/lib/pipeline/template-manager.js +525 -0
- package/lib/pipeline/xss-scanner.js +353 -0
- package/lib/setup-wizard.js +288 -0
- package/package.json +31 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CIPHER GitHub Actions Integration — PR-Level Security Review.
|
|
7
|
+
*
|
|
8
|
+
* Analyzes PR diffs for security-relevant changes, detects hardcoded secrets,
|
|
9
|
+
* identifies new attack surface, and posts structured findings as PR comments.
|
|
10
|
+
* Supports SARIF output for GitHub Code Scanning and generates reusable
|
|
11
|
+
* workflow/action YAML for GitHub Marketplace distribution.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { SarifReport, SarifRule, SarifResult } from './sarif.js';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function _getVersion() {
|
|
24
|
+
try {
|
|
25
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dirname || '.', '..', '..', 'package.json'), 'utf8'));
|
|
26
|
+
return pkg.version || '0.0.0-dev';
|
|
27
|
+
} catch {
|
|
28
|
+
return '0.0.0-dev';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Enums
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/** @type {Readonly<{CRITICAL: string, HIGH: string, MEDIUM: string, LOW: string, INFO: string}>} */
|
|
37
|
+
const RiskLevel = Object.freeze({
|
|
38
|
+
CRITICAL: 'critical',
|
|
39
|
+
HIGH: 'high',
|
|
40
|
+
MEDIUM: 'medium',
|
|
41
|
+
LOW: 'low',
|
|
42
|
+
INFO: 'info',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** @type {Readonly<{API_KEY: string, PASSWORD: string, TOKEN: string, PRIVATE_KEY: string, CONNECTION_STRING: string, AWS_CREDENTIAL: string, GENERIC: string}>} */
|
|
46
|
+
const SecretType = Object.freeze({
|
|
47
|
+
API_KEY: 'api_key',
|
|
48
|
+
PASSWORD: 'password',
|
|
49
|
+
TOKEN: 'token',
|
|
50
|
+
PRIVATE_KEY: 'private_key',
|
|
51
|
+
CONNECTION_STRING: 'connection_string',
|
|
52
|
+
AWS_CREDENTIAL: 'aws_credential',
|
|
53
|
+
GENERIC: 'generic',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Data classes
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
class SecretFinding {
|
|
61
|
+
/**
|
|
62
|
+
* @param {object} opts
|
|
63
|
+
* @param {string} opts.secretType - one of SecretType values
|
|
64
|
+
* @param {string} opts.filePath
|
|
65
|
+
* @param {number} opts.lineNumber
|
|
66
|
+
* @param {string} opts.matchedPattern
|
|
67
|
+
* @param {string} opts.snippet
|
|
68
|
+
* @param {number} opts.confidence
|
|
69
|
+
* @param {string} [opts.recommendation]
|
|
70
|
+
*/
|
|
71
|
+
constructor(opts = {}) {
|
|
72
|
+
this.secretType = opts.secretType || SecretType.GENERIC;
|
|
73
|
+
this.filePath = opts.filePath || '';
|
|
74
|
+
this.lineNumber = opts.lineNumber || 0;
|
|
75
|
+
this.matchedPattern = opts.matchedPattern || '';
|
|
76
|
+
this.snippet = opts.snippet || '';
|
|
77
|
+
this.confidence = opts.confidence || 0;
|
|
78
|
+
this.recommendation =
|
|
79
|
+
opts.recommendation || 'Rotate immediately and move to a secrets manager.';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get fingerprint() {
|
|
83
|
+
const raw = `${this.secretType}:${this.filePath}:${this.matchedPattern}`;
|
|
84
|
+
return createHash('sha256').update(raw).digest('hex').slice(0, 16);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class DiffAnalysis {
|
|
89
|
+
constructor(opts = {}) {
|
|
90
|
+
this.filesChanged = opts.filesChanged || [];
|
|
91
|
+
this.endpointsAdded = opts.endpointsAdded || [];
|
|
92
|
+
this.endpointsModified = opts.endpointsModified || [];
|
|
93
|
+
this.secrets = opts.secrets || [];
|
|
94
|
+
this.authChanges = opts.authChanges || [];
|
|
95
|
+
this.cryptoChanges = opts.cryptoChanges || [];
|
|
96
|
+
this.sqlChanges = opts.sqlChanges || [];
|
|
97
|
+
this.inputChanges = opts.inputChanges || [];
|
|
98
|
+
this.fileChanges = opts.fileChanges || [];
|
|
99
|
+
this.networkChanges = opts.networkChanges || [];
|
|
100
|
+
this.dependencyChanges = opts.dependencyChanges || [];
|
|
101
|
+
this.riskLevel = opts.riskLevel || RiskLevel.INFO;
|
|
102
|
+
this.summary = opts.summary || '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get hasSecurityFindings() {
|
|
106
|
+
return !!(
|
|
107
|
+
this.secrets.length ||
|
|
108
|
+
this.authChanges.length ||
|
|
109
|
+
this.cryptoChanges.length ||
|
|
110
|
+
this.sqlChanges.length
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
class PRReviewResult {
|
|
116
|
+
constructor(opts = {}) {
|
|
117
|
+
this.repo = opts.repo || '';
|
|
118
|
+
this.prNumber = opts.prNumber || 0;
|
|
119
|
+
this.diffAnalysis = opts.diffAnalysis || new DiffAnalysis();
|
|
120
|
+
this.scanFindings = opts.scanFindings || [];
|
|
121
|
+
this.regressions = opts.regressions || [];
|
|
122
|
+
this.reviewedAt = opts.reviewedAt || new Date().toISOString();
|
|
123
|
+
this.reviewId = opts.reviewId || randomUUID().replace(/-/g, '').slice(0, 12);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get riskLevel() {
|
|
127
|
+
return this.diffAnalysis.riskLevel;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get totalFindings() {
|
|
131
|
+
return (
|
|
132
|
+
this.diffAnalysis.secrets.length +
|
|
133
|
+
this.scanFindings.length +
|
|
134
|
+
this.regressions.length
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Regex patterns — compiled once at module load
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
const _AUTH_RE = [
|
|
144
|
+
/\b(authenticate|authorize|login|logout|session|jwt|oauth)\b/i,
|
|
145
|
+
/\b(password|passwd|credential|api_key|secret)\b/i,
|
|
146
|
+
/@(require_auth|login_required|permission_required)/i,
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const _CRYPTO_RE = [
|
|
150
|
+
/\b(md5|sha1|sha256|sha512|hmac|bcrypt|scrypt|argon2)\b/i,
|
|
151
|
+
/\b(encrypt|decrypt|cipher|aes|rsa|ecdsa|sign|verify)\b/i,
|
|
152
|
+
/\b(ssl|tls|certificate|x509|pem|pkcs)\b/i,
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const _SQL_RE = [
|
|
156
|
+
/\b(execute|raw_sql|text\(|cursor\.execute)\b/i,
|
|
157
|
+
/(SELECT|INSERT|UPDATE|DELETE|DROP|ALTER)\s+/i,
|
|
158
|
+
/f['"].*\{.*\}.*(?:SELECT|INSERT|UPDATE|DELETE)/i,
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const _INPUT_RE = [
|
|
162
|
+
/\b(request\.(get|post|json|form|args|params))\b/i,
|
|
163
|
+
/\b(innerHTML|dangerouslySetInnerHTML|eval|exec)\b/i,
|
|
164
|
+
/\b(unserialize|pickle\.loads|yaml\.load)\b/i,
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const _FILE_RE = [
|
|
168
|
+
/\b(subprocess|os\.system|popen|exec|spawn)\b/i,
|
|
169
|
+
/\b(open|read|write|unlink|rmtree|chmod|chown)\b/i,
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const _NETWORK_RE = [
|
|
173
|
+
/\b(fetch|axios|request|http\.get|https\.get)\b/i,
|
|
174
|
+
/\b(WebSocket|XMLHttpRequest|cors)\b/i,
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const _DEPENDENCY_RE = [
|
|
178
|
+
/\b(require|import)\b/i,
|
|
179
|
+
/\b(dependencies|devDependencies|peerDependencies)\b/i,
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
const _ENDPOINT_RE = [
|
|
183
|
+
/@(?:app|router|blueprint)\.(get|post|put|patch|delete)\(['"]([^'"]+)/i,
|
|
184
|
+
/path\(['"]([^'"]+)['"]/i,
|
|
185
|
+
/router\.(get|post|put|patch|delete)\(['"]([^'"]+)/i,
|
|
186
|
+
/@(Get|Post|Put|Patch|Delete)Mapping\(['"]([^'"]+)/i,
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
/** @type {Array<[RegExp, string, number]>} */
|
|
190
|
+
const _SECRET_RE = [
|
|
191
|
+
[/AKIA[0-9A-Z]{16}/, SecretType.AWS_CREDENTIAL, 0.95],
|
|
192
|
+
[/-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/, SecretType.PRIVATE_KEY, 0.99],
|
|
193
|
+
[/ghp_[A-Za-z0-9_]{36}/, SecretType.TOKEN, 0.95],
|
|
194
|
+
[/sk-[A-Za-z0-9]{48}/, SecretType.API_KEY, 0.90],
|
|
195
|
+
[/xox[bpas]-[A-Za-z0-9\-]{10,}/, SecretType.TOKEN, 0.90],
|
|
196
|
+
[/(?:api[_-]?key|apikey)\s*[:=]\s*['"]([A-Za-z0-9_\-]{20,})['"]/i, SecretType.API_KEY, 0.75],
|
|
197
|
+
[/(?:password|passwd|pwd)\s*[:=]\s*['"]([^'"]{8,})['"]/i, SecretType.PASSWORD, 0.70],
|
|
198
|
+
[/(?:secret|token)\s*[:=]\s*['"]([A-Za-z0-9_\-]{16,})['"]/i, SecretType.TOKEN, 0.70],
|
|
199
|
+
[/(?:postgres|mysql|mongodb):\/\/[^\s'"]{10,}/i, SecretType.CONNECTION_STRING, 0.85],
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Severity emoji map
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
const SEV_EMOJI = Object.freeze({
|
|
207
|
+
critical: '🔴',
|
|
208
|
+
high: '🟠',
|
|
209
|
+
medium: '🟡',
|
|
210
|
+
low: '🔵',
|
|
211
|
+
info: '⚪',
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// SecurityDiffAnalyzer
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
class SecurityDiffAnalyzer {
|
|
219
|
+
/**
|
|
220
|
+
* Parse a unified diff and identify security-relevant changes.
|
|
221
|
+
* @param {string} diffText
|
|
222
|
+
* @returns {DiffAnalysis}
|
|
223
|
+
*/
|
|
224
|
+
analyzeDiff(diffText) {
|
|
225
|
+
const analysis = new DiffAnalysis({
|
|
226
|
+
filesChanged: this._extractFiles(diffText),
|
|
227
|
+
endpointsAdded: this.extractEndpointsFromDiff(diffText),
|
|
228
|
+
secrets: this.detectSecretsInDiff(diffText),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
for (const line of this._addedLines(diffText)) {
|
|
232
|
+
this._classifyLine(line, analysis);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
analysis.riskLevel = this._riskFrom(analysis);
|
|
236
|
+
analysis.summary = this._summarize(analysis);
|
|
237
|
+
return analysis;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Extract new/modified API endpoint paths from added diff lines.
|
|
242
|
+
* @param {string} diffText
|
|
243
|
+
* @returns {string[]}
|
|
244
|
+
*/
|
|
245
|
+
extractEndpointsFromDiff(diffText) {
|
|
246
|
+
const endpoints = [];
|
|
247
|
+
for (const line of this._addedLines(diffText)) {
|
|
248
|
+
for (const pat of _ENDPOINT_RE) {
|
|
249
|
+
const m = pat.exec(line);
|
|
250
|
+
if (m) {
|
|
251
|
+
const path = m[m.length > 2 ? 2 : 1];
|
|
252
|
+
if (path && !endpoints.includes(path)) {
|
|
253
|
+
endpoints.push(path);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return endpoints;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Scan added lines for hardcoded secrets and credentials.
|
|
263
|
+
* @param {string} diffText
|
|
264
|
+
* @returns {SecretFinding[]}
|
|
265
|
+
*/
|
|
266
|
+
detectSecretsInDiff(diffText) {
|
|
267
|
+
const findings = [];
|
|
268
|
+
let curFile = '<unknown>';
|
|
269
|
+
let lnum = 0;
|
|
270
|
+
|
|
271
|
+
for (const raw of diffText.split('\n')) {
|
|
272
|
+
if (raw.startsWith('+++ b/')) {
|
|
273
|
+
curFile = raw.slice(6);
|
|
274
|
+
} else if (raw.startsWith('@@ ')) {
|
|
275
|
+
const m = raw.match(/\+(\d+)/);
|
|
276
|
+
lnum = m ? parseInt(m[1], 10) : 0;
|
|
277
|
+
} else if (raw.startsWith('+') && !raw.startsWith('+++')) {
|
|
278
|
+
lnum += 1;
|
|
279
|
+
const content = raw.slice(1);
|
|
280
|
+
for (const [pattern, stype, conf] of _SECRET_RE) {
|
|
281
|
+
if (pattern.test(content)) {
|
|
282
|
+
findings.push(
|
|
283
|
+
new SecretFinding({
|
|
284
|
+
secretType: stype,
|
|
285
|
+
filePath: curFile,
|
|
286
|
+
lineNumber: lnum,
|
|
287
|
+
matchedPattern: pattern.source.slice(0, 60),
|
|
288
|
+
snippet: content.trim().slice(0, 80),
|
|
289
|
+
confidence: conf,
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} else if (!raw.startsWith('-')) {
|
|
296
|
+
lnum += 1;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return findings;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Classify risk from raw diff text.
|
|
304
|
+
* @param {string} diffText
|
|
305
|
+
* @returns {string}
|
|
306
|
+
*/
|
|
307
|
+
classifyRisk(diffText) {
|
|
308
|
+
return this.analyzeDiff(diffText).riskLevel;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Extract changed file paths from diff.
|
|
313
|
+
* @param {string} diff
|
|
314
|
+
* @returns {string[]}
|
|
315
|
+
*/
|
|
316
|
+
_extractFiles(diff) {
|
|
317
|
+
return diff
|
|
318
|
+
.split('\n')
|
|
319
|
+
.filter((ln) => ln.startsWith('+++ b/'))
|
|
320
|
+
.map((ln) => ln.slice(6));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Extract added lines (lines starting with '+' excluding file headers).
|
|
325
|
+
* @param {string} diff
|
|
326
|
+
* @returns {string[]}
|
|
327
|
+
*/
|
|
328
|
+
_addedLines(diff) {
|
|
329
|
+
return diff
|
|
330
|
+
.split('\n')
|
|
331
|
+
.filter((ln) => ln.startsWith('+') && !ln.startsWith('+++'))
|
|
332
|
+
.map((ln) => ln.slice(1));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Classify a single diff line into analysis buckets.
|
|
337
|
+
* @param {string} line
|
|
338
|
+
* @param {DiffAnalysis} a
|
|
339
|
+
*/
|
|
340
|
+
_classifyLine(line, a) {
|
|
341
|
+
const stripped = line.trim();
|
|
342
|
+
const buckets = [
|
|
343
|
+
[_AUTH_RE, a.authChanges],
|
|
344
|
+
[_CRYPTO_RE, a.cryptoChanges],
|
|
345
|
+
[_SQL_RE, a.sqlChanges],
|
|
346
|
+
[_INPUT_RE, a.inputChanges],
|
|
347
|
+
[_FILE_RE, a.fileChanges],
|
|
348
|
+
[_NETWORK_RE, a.networkChanges],
|
|
349
|
+
[_DEPENDENCY_RE, a.dependencyChanges],
|
|
350
|
+
];
|
|
351
|
+
for (const [pats, bucket] of buckets) {
|
|
352
|
+
if (pats.some((p) => p.test(line))) {
|
|
353
|
+
bucket.push(stripped);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Determine risk level from analysis.
|
|
360
|
+
* @param {DiffAnalysis} a
|
|
361
|
+
* @returns {string}
|
|
362
|
+
*/
|
|
363
|
+
_riskFrom(a) {
|
|
364
|
+
if (a.secrets.length || a.authChanges.length || a.cryptoChanges.length) {
|
|
365
|
+
return RiskLevel.HIGH;
|
|
366
|
+
}
|
|
367
|
+
if (a.sqlChanges.length || a.inputChanges.length) {
|
|
368
|
+
return RiskLevel.MEDIUM;
|
|
369
|
+
}
|
|
370
|
+
if (a.fileChanges.length || a.endpointsAdded.length) {
|
|
371
|
+
return RiskLevel.LOW;
|
|
372
|
+
}
|
|
373
|
+
return RiskLevel.INFO;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Produce human-readable summary.
|
|
378
|
+
* @param {DiffAnalysis} a
|
|
379
|
+
* @returns {string}
|
|
380
|
+
*/
|
|
381
|
+
_summarize(a) {
|
|
382
|
+
const parts = [`${a.filesChanged.length} file(s) changed`];
|
|
383
|
+
const checks = [
|
|
384
|
+
['secret(s)', a.secrets],
|
|
385
|
+
['auth change(s)', a.authChanges],
|
|
386
|
+
['crypto change(s)', a.cryptoChanges],
|
|
387
|
+
['SQL change(s)', a.sqlChanges],
|
|
388
|
+
['new endpoint(s)', a.endpointsAdded],
|
|
389
|
+
];
|
|
390
|
+
for (const [label, items] of checks) {
|
|
391
|
+
if (items.length) {
|
|
392
|
+
parts.push(`${items.length} ${label}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return parts.join('; ');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// FindingFormatter
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
class FindingFormatter {
|
|
404
|
+
/**
|
|
405
|
+
* Format a full PR review result as a GitHub markdown comment.
|
|
406
|
+
* @param {PRReviewResult} findings
|
|
407
|
+
* @returns {string}
|
|
408
|
+
*/
|
|
409
|
+
toPrComment(findings) {
|
|
410
|
+
const da = findings.diffAnalysis;
|
|
411
|
+
const risk = findings.riskLevel;
|
|
412
|
+
const lines = [
|
|
413
|
+
`## ${SEV_EMOJI[risk] || '⚪'} CIPHER Security Review — ${risk.toUpperCase()} risk`,
|
|
414
|
+
'',
|
|
415
|
+
`> Review \`${findings.reviewId}\` | ${findings.reviewedAt}`,
|
|
416
|
+
'',
|
|
417
|
+
`**Summary:** ${da.summary}`,
|
|
418
|
+
'',
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
if (da.secrets.length) {
|
|
422
|
+
lines.push(
|
|
423
|
+
'### 🔑 Hardcoded Secrets',
|
|
424
|
+
'',
|
|
425
|
+
'| # | Type | File | Line | Confidence |',
|
|
426
|
+
'|---|------|------|------|------------|',
|
|
427
|
+
);
|
|
428
|
+
da.secrets.forEach((s, i) => {
|
|
429
|
+
lines.push(
|
|
430
|
+
`| ${i + 1} | \`${s.secretType}\` | \`${s.filePath}\` | ${s.lineNumber} | ${Math.round(s.confidence * 100)}% |`,
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
lines.push('');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const sections = [
|
|
437
|
+
['🔐 Auth Changes', da.authChanges],
|
|
438
|
+
['🔒 Crypto Changes', da.cryptoChanges],
|
|
439
|
+
['🗄️ SQL Changes', da.sqlChanges],
|
|
440
|
+
['📥 Input Handling', da.inputChanges],
|
|
441
|
+
['📂 File/Process Ops', da.fileChanges],
|
|
442
|
+
];
|
|
443
|
+
for (const [label, items] of sections) {
|
|
444
|
+
if (items.length) {
|
|
445
|
+
lines.push(`### ${label}`, '');
|
|
446
|
+
for (const it of items.slice(0, 10)) {
|
|
447
|
+
lines.push(`- \`${it.slice(0, 120)}\``);
|
|
448
|
+
}
|
|
449
|
+
if (items.length > 10) {
|
|
450
|
+
lines.push(`- _… and ${items.length - 10} more_`);
|
|
451
|
+
}
|
|
452
|
+
lines.push('');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (da.endpointsAdded.length) {
|
|
457
|
+
lines.push('### 🌐 New Endpoints', '');
|
|
458
|
+
for (const ep of da.endpointsAdded) {
|
|
459
|
+
lines.push(`- \`${ep}\``);
|
|
460
|
+
}
|
|
461
|
+
lines.push('');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (findings.regressions.length) {
|
|
465
|
+
lines.push('### ⚠️ Regressions', '');
|
|
466
|
+
for (const r of findings.regressions) {
|
|
467
|
+
lines.push(`- **${r.title || 'Untitled'}** — ${r.description || ''}`);
|
|
468
|
+
}
|
|
469
|
+
lines.push('');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
lines.push('---', '_Generated by [CIPHER](https://github.com/defconxt/cipher)_');
|
|
473
|
+
return lines.join('\n');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Produce SARIF v2.1.0 for GitHub Code Scanning.
|
|
478
|
+
* @param {PRReviewResult} findings
|
|
479
|
+
* @returns {object}
|
|
480
|
+
*/
|
|
481
|
+
toSarif(findings) {
|
|
482
|
+
const results = [];
|
|
483
|
+
const rules = [];
|
|
484
|
+
const seen = new Set();
|
|
485
|
+
|
|
486
|
+
for (const s of findings.diffAnalysis.secrets) {
|
|
487
|
+
const rid = `cipher/secret-${s.secretType}`;
|
|
488
|
+
if (!seen.has(rid)) {
|
|
489
|
+
seen.add(rid);
|
|
490
|
+
rules.push({
|
|
491
|
+
id: rid,
|
|
492
|
+
name: `HardcodedSecret_${s.secretType}`,
|
|
493
|
+
shortDescription: { text: `Hardcoded ${s.secretType}` },
|
|
494
|
+
defaultConfiguration: { level: 'error' },
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
results.push({
|
|
498
|
+
ruleId: rid,
|
|
499
|
+
message: { text: `Hardcoded ${s.secretType} in diff` },
|
|
500
|
+
locations: [
|
|
501
|
+
{
|
|
502
|
+
physicalLocation: {
|
|
503
|
+
artifactLocation: { uri: s.filePath },
|
|
504
|
+
region: { startLine: s.lineNumber },
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
fingerprints: { 'cipher/v1': s.fingerprint },
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
$schema:
|
|
514
|
+
'https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-schema-2.1.0.json',
|
|
515
|
+
version: '2.1.0',
|
|
516
|
+
runs: [
|
|
517
|
+
{
|
|
518
|
+
tool: {
|
|
519
|
+
driver: {
|
|
520
|
+
name: 'CIPHER',
|
|
521
|
+
version: _getVersion(),
|
|
522
|
+
informationUri: 'https://github.com/defconxt/cipher',
|
|
523
|
+
rules,
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
results,
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Serialize review result as structured JSON.
|
|
534
|
+
* @param {PRReviewResult} findings
|
|
535
|
+
* @returns {string}
|
|
536
|
+
*/
|
|
537
|
+
toJson(findings) {
|
|
538
|
+
const da = findings.diffAnalysis;
|
|
539
|
+
return JSON.stringify(
|
|
540
|
+
{
|
|
541
|
+
review_id: findings.reviewId,
|
|
542
|
+
repo: findings.repo,
|
|
543
|
+
pr_number: findings.prNumber,
|
|
544
|
+
risk_level: findings.riskLevel,
|
|
545
|
+
reviewed_at: findings.reviewedAt,
|
|
546
|
+
total_findings: findings.totalFindings,
|
|
547
|
+
diff_analysis: {
|
|
548
|
+
files_changed: da.filesChanged,
|
|
549
|
+
endpoints_added: da.endpointsAdded,
|
|
550
|
+
secrets_count: da.secrets.length,
|
|
551
|
+
auth_changes: da.authChanges.length,
|
|
552
|
+
crypto_changes: da.cryptoChanges.length,
|
|
553
|
+
sql_changes: da.sqlChanges.length,
|
|
554
|
+
},
|
|
555
|
+
secrets: da.secrets.map((s) => ({
|
|
556
|
+
type: s.secretType,
|
|
557
|
+
file: s.filePath,
|
|
558
|
+
line: s.lineNumber,
|
|
559
|
+
confidence: s.confidence,
|
|
560
|
+
fingerprint: s.fingerprint,
|
|
561
|
+
})),
|
|
562
|
+
regressions: findings.regressions,
|
|
563
|
+
},
|
|
564
|
+
null,
|
|
565
|
+
2,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
// PRSecurityReview — orchestrates PR-level reviews via GitHub API
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
|
|
574
|
+
class PRSecurityReview {
|
|
575
|
+
/**
|
|
576
|
+
* @param {object} [opts]
|
|
577
|
+
* @param {string} [opts.githubToken]
|
|
578
|
+
* @param {string} [opts.baseUrl]
|
|
579
|
+
*/
|
|
580
|
+
constructor(opts = {}) {
|
|
581
|
+
this.githubToken = opts.githubToken || '';
|
|
582
|
+
this._api = opts.baseUrl || 'https://api.github.com';
|
|
583
|
+
this._analyzer = new SecurityDiffAnalyzer();
|
|
584
|
+
this._formatter = new FindingFormatter();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Fetch PR diff, analyse, and return review result.
|
|
589
|
+
* @param {string} repo
|
|
590
|
+
* @param {number} prNumber
|
|
591
|
+
* @returns {Promise<PRReviewResult>}
|
|
592
|
+
*/
|
|
593
|
+
async reviewPr(repo, prNumber) {
|
|
594
|
+
const diffText = await this._fetchDiff(repo, prNumber);
|
|
595
|
+
const analysis = this._analyzer.analyzeDiff(diffText);
|
|
596
|
+
return new PRReviewResult({
|
|
597
|
+
repo,
|
|
598
|
+
prNumber,
|
|
599
|
+
diffAnalysis: analysis,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Post findings as a PR comment.
|
|
605
|
+
* @param {string} repo
|
|
606
|
+
* @param {number} prNumber
|
|
607
|
+
* @param {PRReviewResult} findings
|
|
608
|
+
* @returns {Promise<boolean>}
|
|
609
|
+
*/
|
|
610
|
+
async postComment(repo, prNumber, findings) {
|
|
611
|
+
const body = this._formatter.toPrComment(findings);
|
|
612
|
+
const url = `${this._api}/repos/${repo}/issues/${prNumber}/comments`;
|
|
613
|
+
return this._post(url, { body });
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* @param {string} repo
|
|
618
|
+
* @param {number} prNumber
|
|
619
|
+
* @returns {Promise<string>}
|
|
620
|
+
*/
|
|
621
|
+
async _fetchDiff(repo, prNumber) {
|
|
622
|
+
const url = `${this._api}/repos/${repo}/pulls/${prNumber}`;
|
|
623
|
+
const headers = { Accept: 'application/vnd.github.diff' };
|
|
624
|
+
if (this.githubToken) {
|
|
625
|
+
headers.Authorization = `Bearer ${this.githubToken}`;
|
|
626
|
+
}
|
|
627
|
+
const resp = await fetch(url, { headers, signal: AbortSignal.timeout(30000) });
|
|
628
|
+
return resp.text();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* @param {string} url
|
|
633
|
+
* @param {object} payload
|
|
634
|
+
* @returns {Promise<boolean>}
|
|
635
|
+
*/
|
|
636
|
+
async _post(url, payload) {
|
|
637
|
+
const headers = {
|
|
638
|
+
Accept: 'application/vnd.github+json',
|
|
639
|
+
'Content-Type': 'application/json',
|
|
640
|
+
};
|
|
641
|
+
if (this.githubToken) {
|
|
642
|
+
headers.Authorization = `Bearer ${this.githubToken}`;
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
const resp = await fetch(url, {
|
|
646
|
+
method: 'POST',
|
|
647
|
+
headers,
|
|
648
|
+
body: JSON.stringify(payload),
|
|
649
|
+
signal: AbortSignal.timeout(30000),
|
|
650
|
+
});
|
|
651
|
+
return resp.status >= 200 && resp.status < 300;
|
|
652
|
+
} catch {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ---------------------------------------------------------------------------
|
|
659
|
+
// WorkflowGenerator
|
|
660
|
+
// ---------------------------------------------------------------------------
|
|
661
|
+
|
|
662
|
+
const _WORKFLOW_TPL = `# Generated by CIPHER — do not edit manually.
|
|
663
|
+
name: CIPHER Security Review
|
|
664
|
+
on:
|
|
665
|
+
pull_request:
|
|
666
|
+
types: [opened, synchronize, reopened]
|
|
667
|
+
workflow_dispatch:
|
|
668
|
+
inputs:
|
|
669
|
+
scan_profile:
|
|
670
|
+
description: "Scan profile"
|
|
671
|
+
default: "{{profile}}"
|
|
672
|
+
type: choice
|
|
673
|
+
options: [pentest, recon, architecture]
|
|
674
|
+
permissions: { contents: read, pull-requests: write, security-events: write }
|
|
675
|
+
jobs:
|
|
676
|
+
security-review:
|
|
677
|
+
runs-on: ubuntu-latest
|
|
678
|
+
steps:
|
|
679
|
+
- uses: actions/checkout@v4
|
|
680
|
+
with: { fetch-depth: 0 }
|
|
681
|
+
- uses: actions/setup-python@v5
|
|
682
|
+
with: { python-version: "3.12" }
|
|
683
|
+
- run: pip install cipher-security
|
|
684
|
+
- name: Run PR Security Review
|
|
685
|
+
env:
|
|
686
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
687
|
+
CIPHER_PROFILE: \${{ inputs.scan_profile || '{{profile}}' }}
|
|
688
|
+
run: >-
|
|
689
|
+
cipher review-pr
|
|
690
|
+
--repo "\${{ github.repository }}"
|
|
691
|
+
--pr "\${{ github.event.pull_request.number }}"
|
|
692
|
+
--profile "$CIPHER_PROFILE"
|
|
693
|
+
--output sarif --output-file results.sarif
|
|
694
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
695
|
+
if: always()
|
|
696
|
+
with: { sarif_file: results.sarif }
|
|
697
|
+
- name: Post PR Comment
|
|
698
|
+
if: github.event_name == 'pull_request'
|
|
699
|
+
env:
|
|
700
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
701
|
+
run: >-
|
|
702
|
+
cipher review-pr
|
|
703
|
+
--repo "\${{ github.repository }}"
|
|
704
|
+
--pr "\${{ github.event.pull_request.number }}"
|
|
705
|
+
--comment
|
|
706
|
+
`;
|
|
707
|
+
|
|
708
|
+
const _ACTION_TPL = `# Generated by CIPHER — do not edit manually.
|
|
709
|
+
name: "CIPHER Security Review"
|
|
710
|
+
description: "Automated PR-level security review. Detects secrets, auth regressions, new attack surface."
|
|
711
|
+
author: "defconxt"
|
|
712
|
+
branding: { icon: shield, color: purple }
|
|
713
|
+
inputs:
|
|
714
|
+
scan_profile: { description: "Scan profile", required: false, default: "pentest" }
|
|
715
|
+
github_token: { description: "GitHub token", required: true }
|
|
716
|
+
post_comment: { description: "Post findings as PR comment", required: false, default: "true" }
|
|
717
|
+
sarif_output: { description: "SARIF output path", required: false, default: "cipher-results.sarif" }
|
|
718
|
+
outputs:
|
|
719
|
+
risk_level: { description: "Overall risk level (high|medium|low|info)" }
|
|
720
|
+
total_findings: { description: "Total number of findings" }
|
|
721
|
+
review_id: { description: "Unique review identifier" }
|
|
722
|
+
runs:
|
|
723
|
+
using: "composite"
|
|
724
|
+
steps:
|
|
725
|
+
- uses: actions/setup-python@v5
|
|
726
|
+
with: { python-version: "3.12" }
|
|
727
|
+
- { shell: bash, run: "pip install cipher-security" }
|
|
728
|
+
- name: Run Security Review
|
|
729
|
+
shell: bash
|
|
730
|
+
env:
|
|
731
|
+
GITHUB_TOKEN: \${{ inputs.github_token }}
|
|
732
|
+
CIPHER_PROFILE: \${{ inputs.scan_profile }}
|
|
733
|
+
run: >-
|
|
734
|
+
cipher review-pr
|
|
735
|
+
--repo "\${{ github.repository }}"
|
|
736
|
+
--pr "\${{ github.event.pull_request.number }}"
|
|
737
|
+
--profile "$CIPHER_PROFILE"
|
|
738
|
+
--output sarif --output-file "\${{ inputs.sarif_output }}"
|
|
739
|
+
- name: Post Comment
|
|
740
|
+
if: inputs.post_comment == 'true'
|
|
741
|
+
shell: bash
|
|
742
|
+
env: { GITHUB_TOKEN: "\${{ inputs.github_token }}" }
|
|
743
|
+
run: >-
|
|
744
|
+
cipher review-pr
|
|
745
|
+
--repo "\${{ github.repository }}"
|
|
746
|
+
--pr "\${{ github.event.pull_request.number }}"
|
|
747
|
+
--comment
|
|
748
|
+
`;
|
|
749
|
+
|
|
750
|
+
class WorkflowGenerator {
|
|
751
|
+
/**
|
|
752
|
+
* Generate `.github/workflows/cipher-security.yml`.
|
|
753
|
+
* @param {string} [scanProfile='pentest']
|
|
754
|
+
* @returns {string}
|
|
755
|
+
*/
|
|
756
|
+
generateWorkflow(scanProfile = 'pentest') {
|
|
757
|
+
return _WORKFLOW_TPL.replace(/\{\{profile\}\}/g, scanProfile);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Generate `action.yml` for GitHub Marketplace.
|
|
762
|
+
* @returns {string}
|
|
763
|
+
*/
|
|
764
|
+
generateActionYaml() {
|
|
765
|
+
return _ACTION_TPL;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ---------------------------------------------------------------------------
|
|
770
|
+
// Exports
|
|
771
|
+
// ---------------------------------------------------------------------------
|
|
772
|
+
|
|
773
|
+
export {
|
|
774
|
+
// Helpers
|
|
775
|
+
_getVersion,
|
|
776
|
+
// Enums
|
|
777
|
+
RiskLevel,
|
|
778
|
+
SecretType,
|
|
779
|
+
SEV_EMOJI,
|
|
780
|
+
// Data classes
|
|
781
|
+
SecretFinding,
|
|
782
|
+
DiffAnalysis,
|
|
783
|
+
PRReviewResult,
|
|
784
|
+
// Analyzers
|
|
785
|
+
SecurityDiffAnalyzer,
|
|
786
|
+
// Formatters
|
|
787
|
+
FindingFormatter,
|
|
788
|
+
// PR Review
|
|
789
|
+
PRSecurityReview,
|
|
790
|
+
// Generators
|
|
791
|
+
WorkflowGenerator,
|
|
792
|
+
};
|