|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +/** |
| 4 | + * Integration tests for `sigmap redact` (v8.24 G3a). |
| 5 | + * |
| 6 | + * Tests: |
| 7 | + * 1. redactText masks only the matched substring (surrounding text kept) |
| 8 | + * 2. every pattern in security/patterns.js is masked by redactText |
| 9 | + * 3. findings carry 1-based line numbers; counts aggregate per pattern |
| 10 | + * 4. clean text passes through byte-identical with redacted: false |
| 11 | + * 5. CLI redact <file>: redacted text on stdout, summary on stderr |
| 12 | + * 6. CLI stdin + --json returns the result object |
| 13 | + */ |
| 14 | + |
| 15 | +const assert = require('assert'); |
| 16 | +const fs = require('fs'); |
| 17 | +const os = require('os'); |
| 18 | +const path = require('path'); |
| 19 | +const { spawnSync } = require('child_process'); |
| 20 | + |
| 21 | +const ROOT = path.resolve(__dirname, '../..'); |
| 22 | +const SCRIPT = path.join(ROOT, 'gen-context.js'); |
| 23 | + |
| 24 | +let passed = 0; |
| 25 | +let failed = 0; |
| 26 | + |
| 27 | +function test(name, fn) { |
| 28 | + try { |
| 29 | + fn(); |
| 30 | + console.log(` PASS ${name}`); |
| 31 | + passed++; |
| 32 | + } catch (err) { |
| 33 | + console.log(` FAIL ${name}: ${err.message}`); |
| 34 | + failed++; |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +const { redactText } = require(path.join(ROOT, 'src', 'security', 'redact')); |
| 39 | +const { PATTERNS } = require(path.join(ROOT, 'src', 'security', 'patterns')); |
| 40 | + |
| 41 | +// One realistic sample per pattern name. |
| 42 | +// Samples are assembled at runtime so no secret-shaped literal exists in this |
| 43 | +// file — GitHub Push Protection scans committed blobs and rejects otherwise. |
| 44 | +const AL = 'abcdefghijklmnopqrstuvwxyz'; |
| 45 | +const SAMPLES = { |
| 46 | + 'AWS Access Key': `key ${'AK' + 'IA'}1234567890ABCDEF end`, |
| 47 | + 'AWS Secret Key': `aws_secret = "${AL + AL.toUpperCase().slice(0, 10)}0123`.padEnd(52, '4') + '"', |
| 48 | + 'GCP API Key': `k ${'AI' + 'za'}AbCdEfGhIjKlMnOpQrStUvWxYz0123456789 z`, |
| 49 | + 'GitHub Token': 'gh' + 'p_' + AL + '0123456789', |
| 50 | + 'JWT Token': `bearer ${'ey' + 'J'}hbGciOiJIUzI1NiJ9.${'ey' + 'J'}zdWIiOiIxIn0.abc-123_x`, |
| 51 | + 'DB Connection String': `url ${'postgres' + '://'}admin:hunter2@db.example.com/prod`, |
| 52 | + 'SSH Private Key': ['-----BEGIN', 'RSA PRIVATE', 'KEY-----'].join(' '), |
| 53 | + 'Stripe Key': 'sk_' + 'live_' + AL.slice(0, 24), |
| 54 | + 'Twilio Key': `sid ${'S' + 'K'}0123456789abcdef0123456789abcdef done`, |
| 55 | + 'Generic Secret': `password = ${'"correct-horse-battery"'}`, |
| 56 | +}; |
| 57 | + |
| 58 | +console.log('[redact.test.js] v8.24 G3a standalone redaction'); |
| 59 | +console.log(''); |
| 60 | + |
| 61 | +test('masks only the matched substring, keeps surrounding text', () => { |
| 62 | + const r = redactText('before AKIA1234567890ABCDEF after'); |
| 63 | + assert.strictEqual(r.text, 'before [REDACTED:AWS Access Key] after'); |
| 64 | + assert.strictEqual(r.redacted, true); |
| 65 | +}); |
| 66 | + |
| 67 | +test('every pattern in patterns.js is masked', () => { |
| 68 | + for (const p of PATTERNS) { |
| 69 | + const sample = SAMPLES[p.name]; |
| 70 | + assert.ok(sample, `no sample for pattern "${p.name}" — add one`); |
| 71 | + const r = redactText(sample); |
| 72 | + assert.ok(r.text.includes(`[REDACTED:${p.name}]`), |
| 73 | + `pattern "${p.name}" not masked; got: ${r.text}`); |
| 74 | + } |
| 75 | +}); |
| 76 | + |
| 77 | +test('findings carry 1-based line numbers; counts aggregate', () => { |
| 78 | + const r = redactText('clean\nAKIA1234567890ABCDEF\nAKIAABCDEFGHIJKLMNOP x'); |
| 79 | + assert.deepStrictEqual(r.findings.map((f) => f.line), [2, 3]); |
| 80 | + assert.strictEqual(r.findings[0].pattern, 'AWS Access Key'); |
| 81 | + assert.deepStrictEqual(r.counts, { 'AWS Access Key': 2 }); |
| 82 | +}); |
| 83 | + |
| 84 | +test('clean text passes through byte-identical', () => { |
| 85 | + const input = 'function hello(name) {\n return `hi ${name}`;\n}\n'; |
| 86 | + const r = redactText(input); |
| 87 | + assert.strictEqual(r.text, input); |
| 88 | + assert.strictEqual(r.redacted, false); |
| 89 | + assert.deepStrictEqual(r.findings, []); |
| 90 | +}); |
| 91 | + |
| 92 | +test('CLI redact <file>: stdout redacted, stderr summary', () => { |
| 93 | + const tmp = path.join(os.tmpdir(), `sigmap-redact-${process.pid}.txt`); |
| 94 | + fs.writeFileSync(tmp, 'x AKIA1234567890ABCDEF y\n'); |
| 95 | + try { |
| 96 | + const r = spawnSync(process.execPath, [SCRIPT, 'redact', tmp], { encoding: 'utf8' }); |
| 97 | + assert.strictEqual(r.status, 0, r.stderr); |
| 98 | + assert.strictEqual(r.stdout, 'x [REDACTED:AWS Access Key] y\n'); |
| 99 | + assert.ok(r.stderr.includes('masked 1 secret'), r.stderr); |
| 100 | + } finally { |
| 101 | + fs.unlinkSync(tmp); |
| 102 | + } |
| 103 | +}); |
| 104 | + |
| 105 | +test('CLI stdin + --json returns the result object', () => { |
| 106 | + const r = spawnSync(process.execPath, [SCRIPT, 'redact', '--json'], |
| 107 | + { encoding: 'utf8', input: `token ${'gh' + 'p_'}${AL}0123456789\n` }); |
| 108 | + assert.strictEqual(r.status, 0, r.stderr); |
| 109 | + const j = JSON.parse(r.stdout); |
| 110 | + assert.strictEqual(j.redacted, true); |
| 111 | + assert.deepStrictEqual(j.counts, { 'GitHub Token': 1 }); |
| 112 | + assert.ok(j.text.includes('[REDACTED:GitHub Token]')); |
| 113 | +}); |
| 114 | + |
| 115 | +// --------------------------------------------------------------------------- |
| 116 | +// Summary |
| 117 | +// --------------------------------------------------------------------------- |
| 118 | +console.log(''); |
| 119 | +console.log(`[redact.test.js] ${passed} passed, ${failed} failed`); |
| 120 | +if (failed > 0) process.exit(1); |
0 commit comments