cipher-security 5.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.
Files changed (75) hide show
  1. package/bin/cipher.js +465 -0
  2. package/lib/api/billing.js +321 -0
  3. package/lib/api/compliance.js +693 -0
  4. package/lib/api/controls.js +1401 -0
  5. package/lib/api/index.js +49 -0
  6. package/lib/api/marketplace.js +467 -0
  7. package/lib/api/openai-proxy.js +383 -0
  8. package/lib/api/server.js +685 -0
  9. package/lib/autonomous/feedback-loop.js +554 -0
  10. package/lib/autonomous/framework.js +512 -0
  11. package/lib/autonomous/index.js +97 -0
  12. package/lib/autonomous/leaderboard.js +594 -0
  13. package/lib/autonomous/modes/architect.js +412 -0
  14. package/lib/autonomous/modes/blue.js +386 -0
  15. package/lib/autonomous/modes/incident.js +684 -0
  16. package/lib/autonomous/modes/privacy.js +369 -0
  17. package/lib/autonomous/modes/purple.js +294 -0
  18. package/lib/autonomous/modes/recon.js +250 -0
  19. package/lib/autonomous/parallel.js +587 -0
  20. package/lib/autonomous/researcher.js +583 -0
  21. package/lib/autonomous/runner.js +955 -0
  22. package/lib/autonomous/scheduler.js +615 -0
  23. package/lib/autonomous/task-parser.js +127 -0
  24. package/lib/autonomous/validators/forensic.js +266 -0
  25. package/lib/autonomous/validators/osint.js +216 -0
  26. package/lib/autonomous/validators/privacy.js +296 -0
  27. package/lib/autonomous/validators/purple.js +298 -0
  28. package/lib/autonomous/validators/sigma.js +248 -0
  29. package/lib/autonomous/validators/threat-model.js +363 -0
  30. package/lib/benchmark/agent.js +119 -0
  31. package/lib/benchmark/baselines.js +43 -0
  32. package/lib/benchmark/builder.js +143 -0
  33. package/lib/benchmark/config.js +35 -0
  34. package/lib/benchmark/coordinator.js +91 -0
  35. package/lib/benchmark/index.js +20 -0
  36. package/lib/benchmark/llm.js +58 -0
  37. package/lib/benchmark/models.js +137 -0
  38. package/lib/benchmark/reporter.js +103 -0
  39. package/lib/benchmark/runner.js +103 -0
  40. package/lib/benchmark/sandbox.js +96 -0
  41. package/lib/benchmark/scorer.js +32 -0
  42. package/lib/benchmark/solver.js +166 -0
  43. package/lib/benchmark/tools.js +62 -0
  44. package/lib/bot/bot.js +130 -0
  45. package/lib/commands.js +99 -0
  46. package/lib/complexity.js +377 -0
  47. package/lib/config.js +213 -0
  48. package/lib/gateway/client.js +309 -0
  49. package/lib/gateway/commands.js +830 -0
  50. package/lib/gateway/config-validate.js +109 -0
  51. package/lib/gateway/gateway.js +367 -0
  52. package/lib/gateway/index.js +62 -0
  53. package/lib/gateway/mode.js +309 -0
  54. package/lib/gateway/plugins.js +222 -0
  55. package/lib/gateway/prompt.js +214 -0
  56. package/lib/mcp/server.js +262 -0
  57. package/lib/memory/compressor.js +425 -0
  58. package/lib/memory/engine.js +763 -0
  59. package/lib/memory/evolution.js +668 -0
  60. package/lib/memory/index.js +58 -0
  61. package/lib/memory/orchestrator.js +506 -0
  62. package/lib/memory/retriever.js +515 -0
  63. package/lib/memory/synthesizer.js +333 -0
  64. package/lib/pipeline/async-scanner.js +510 -0
  65. package/lib/pipeline/binary-analysis.js +1043 -0
  66. package/lib/pipeline/dom-xss-scanner.js +435 -0
  67. package/lib/pipeline/github-actions.js +792 -0
  68. package/lib/pipeline/index.js +124 -0
  69. package/lib/pipeline/osint.js +498 -0
  70. package/lib/pipeline/sarif.js +373 -0
  71. package/lib/pipeline/scanner.js +880 -0
  72. package/lib/pipeline/template-manager.js +525 -0
  73. package/lib/pipeline/xss-scanner.js +353 -0
  74. package/lib/setup-wizard.js +229 -0
  75. package/package.json +30 -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
+ };