fini-proof 0.3.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/LICENSE +5 -0
- package/README.md +220 -0
- package/action.yml +53 -0
- package/bin/fini-proof.mjs +6 -0
- package/ci/Jenkinsfile +13 -0
- package/ci/Jenkinsfile.daily +23 -0
- package/ci/bitbucket-pipelines-daily.yml +15 -0
- package/ci/bitbucket-pipelines.yml +12 -0
- package/ci/github-actions-daily.yml +31 -0
- package/ci/github-actions-example.yml +14 -0
- package/ci/gitlab-ci-daily.yml +17 -0
- package/ci/gitlab-ci.yml +13 -0
- package/package.json +31 -0
- package/src/cli.mjs +258 -0
- package/src/engines/fail-open.mjs +133 -0
- package/src/engines/gitleaks.mjs +143 -0
- package/src/engines/hollow-tests.mjs +121 -0
- package/src/engines/migrations.mjs +316 -0
- package/src/engines/secrets.mjs +88 -0
- package/src/engines/skip-ratchet.mjs +78 -0
- package/src/engines/tenant-filter.mjs +516 -0
- package/src/engines/vendor/entrypoint-guard-core.mjs +43 -0
- package/src/engines/vendor/migration-invariants-core.mjs +127 -0
- package/src/engines/vendor/secrets-core.mjs +123 -0
- package/src/engines/vendor/vacuous-spec-core.mjs +1066 -0
- package/src/evidence.mjs +130 -0
- package/src/files.mjs +89 -0
- package/src/hooks.mjs +118 -0
- package/src/licence.mjs +85 -0
- package/src/mcp.mjs +230 -0
- package/src/report.mjs +73 -0
- package/src/runner.mjs +306 -0
- package/src/state.mjs +189 -0
- package/src/upload.mjs +85 -0
- package/src/usage.mjs +64 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// PROVENANCE — ported, not rewritten.
|
|
2
|
+
// source : finipe/scripts/ci/check-entrypoint-guard.mjs (ESM entrypoint fail-open detector)
|
|
3
|
+
// commit : finipe 4ac53b367b348d23d000629cb14e19633beb9613 (file last changed in 04152854b)
|
|
4
|
+
// kept : rawGuards() and its helper constants verbatim (lines 60-103 of the source, minus the Finipe
|
|
5
|
+
// CANONICAL_BLOCK text, which is replaced by an empty sentinel: a customer repo has no canonical
|
|
6
|
+
// block, so every argv[1]-vs-own-path compare that is not realpath'd on both sides is reported).
|
|
7
|
+
// changed: the Finipe-only "must carry the byte-identical canonical block" rule and the symlink probe of every
|
|
8
|
+
// guarded file are not ported (running a customer's scripts is out of scope for a static v0.1 check);
|
|
9
|
+
// the symlink repro survives as this engine's behavioural negative control.
|
|
10
|
+
const CANONICAL_BLOCK = '\u0000fini-proof-no-canonical-block\u0000'
|
|
11
|
+
// Built by concatenation so that no source line of THIS file is a raw guard.
|
|
12
|
+
const ARGV1 = 'process.argv' + '[1]'
|
|
13
|
+
const SELF_REFS = ['import.meta.url', '__filename', 'fileURLToPath', 'pathToFileURL']
|
|
14
|
+
const isComment = (t) => t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')
|
|
15
|
+
|
|
16
|
+
/** Raw guard findings in one source text (canonical blocks blanked first, line numbers kept). Pure. */
|
|
17
|
+
export function rawGuards(src) {
|
|
18
|
+
const blank = '\n'.repeat(CANONICAL_BLOCK.split('\n').length - 1)
|
|
19
|
+
const lines = src.split(CANONICAL_BLOCK).join(blank).split('\n')
|
|
20
|
+
const out = []
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const t = lines[i].trim()
|
|
23
|
+
if (!t.includes(ARGV1) || isComment(t)) continue
|
|
24
|
+
const win = lines.slice(Math.max(0, i - 2), i + 3).filter((l) => !isComment(l.trim())).join('\n')
|
|
25
|
+
if (win.includes('===') && SELF_REFS.some((r) => win.includes(r))) out.push({ line: i + 1, text: t.slice(0, 140) })
|
|
26
|
+
}
|
|
27
|
+
// Indirection: `const entry = process.argv[1]` … `path.resolve(entry) === fileURLToPath(import.meta.url)`
|
|
28
|
+
// any number of lines later (scripts/codemods/aggressive-token-enforcement.mjs had exactly this).
|
|
29
|
+
const aliases = new Set()
|
|
30
|
+
for (const l of lines) {
|
|
31
|
+
const m = !isComment(l.trim()) && l.includes(ARGV1) && /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/.exec(l)
|
|
32
|
+
if (m) aliases.add(m[1])
|
|
33
|
+
}
|
|
34
|
+
for (const a of aliases) {
|
|
35
|
+
const re = new RegExp(`\\b${a.replace(/\$/g, '\\$')}\\b`)
|
|
36
|
+
lines.forEach((l, i) => {
|
|
37
|
+
const t = l.trim()
|
|
38
|
+
if (isComment(t) || t.includes(ARGV1) || !re.test(t)) return
|
|
39
|
+
if (t.includes('===') && SELF_REFS.some((r) => t.includes(r)) && !out.some((o) => o.line === i + 1)) out.push({ line: i + 1, text: t.slice(0, 140) })
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
return out
|
|
43
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// PORTED from finipe scripts/ci/lib/migration-invariants.cjs (blob 6440772a, last changed 18ce3086b 2026-09-26,
|
|
2
|
+
// read at finipe commit 4ac53b367b348d23d000629cb14e19633beb9613).
|
|
3
|
+
// Lines 33–135 (LIT, ROUTE, balanced, literalArray, constArray, constString, classesOf, expandStatic) are VERBATIM,
|
|
4
|
+
// converted from CommonJS to ESM exports. expandStatic() expands the statically-resolvable loops of a migration
|
|
5
|
+
// (`for (const t of ['a','b']) { … ${t} … }`) so table-level rules see every table; what stays interpolated is
|
|
6
|
+
// reported as `unresolved` — never read as clean.
|
|
7
|
+
// downReversal() is ADAPTED from evaluateMigration() lines 213–230 (down() must drop what up() created, in
|
|
8
|
+
// reverse order): the Finipe RLS / GRANT / tenant_id-TEXT / finipe_system rules are Finipe-schema-specific and
|
|
9
|
+
// were NOT ported; the problem strings are returned as { rule, table(s) } objects instead of text.
|
|
10
|
+
const LIT = /^\s*(['"`])([A-Za-z0-9_.]+)\1\s*$/;
|
|
11
|
+
|
|
12
|
+
/** The DB-backed tier gate that measures each property class; null = none does. */
|
|
13
|
+
const ROUTE = {
|
|
14
|
+
'force-rls': 'lint:force-rls-coverage',
|
|
15
|
+
grants: 'lint:grants-coverage',
|
|
16
|
+
'policy-shape': 'lint:finipe-system-policy-shape',
|
|
17
|
+
'down-drop': null, // migration:test:roundtrip is continue-on-error in the tier — not a gate
|
|
18
|
+
'create-table': null, // tenant_id shape has no DB gate
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function balanced(text, openIdx, open, close) {
|
|
22
|
+
let depth = 0;
|
|
23
|
+
for (let j = openIdx; j < text.length; j += 1) {
|
|
24
|
+
if (text[j] === open) depth += 1;
|
|
25
|
+
else if (text[j] === close) {
|
|
26
|
+
depth -= 1;
|
|
27
|
+
if (depth === 0) return j;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return -1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `[ 'a', "b", `c`, ] as const` -> ['a','b','c'], or null if any element is not a plain literal. */
|
|
34
|
+
function literalArray(inner) {
|
|
35
|
+
const parts = inner.split(',').map((p) => p.trim()).filter((p) => p.length);
|
|
36
|
+
if (!parts.length) return null;
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const p of parts) {
|
|
39
|
+
const m = LIT.exec(p);
|
|
40
|
+
if (!m) return null;
|
|
41
|
+
out.push(m[2]);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function constArray(src, name) {
|
|
47
|
+
const re = new RegExp(`\\bconst\\s+${name}\\s*(?::[^=]+)?=\\s*\\[`, 'g');
|
|
48
|
+
const m = re.exec(src);
|
|
49
|
+
if (!m) return null;
|
|
50
|
+
const open = m.index + m[0].length - 1;
|
|
51
|
+
const close = balanced(src, open, '[', ']');
|
|
52
|
+
return close < 0 ? null : literalArray(src.slice(open + 1, close));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function constString(src, name) {
|
|
56
|
+
const m = new RegExp(`\\bconst\\s+${name}\\s*(?::\\s*string\\s*)?=\\s*(['"\`])([A-Za-z0-9_.]+)\\1\\s*;`).exec(src);
|
|
57
|
+
return m ? m[2] : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function classesOf(sqlTemplate) {
|
|
61
|
+
const c = [];
|
|
62
|
+
if (/ROW\s+LEVEL\s+SECURITY/i.test(sqlTemplate)) c.push('force-rls');
|
|
63
|
+
if (/\bGRANT\b/i.test(sqlTemplate)) c.push('grants');
|
|
64
|
+
if (/CREATE\s+POLICY/i.test(sqlTemplate)) c.push('policy-shape');
|
|
65
|
+
if (/DROP\s+TABLE/i.test(sqlTemplate)) c.push('down-drop');
|
|
66
|
+
if (/CREATE\s+TABLE/i.test(sqlTemplate)) c.push('create-table');
|
|
67
|
+
return c;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function expandStatic(execText) {
|
|
71
|
+
let text = execText;
|
|
72
|
+
// Loops, LAST first: an inner loop starts after its outer one, so it is expanded before it.
|
|
73
|
+
const heads = [...text.matchAll(/\bfor\s*\(\s*const\s+([A-Za-z_$][\w$]*)\s+of\s+/g)].reverse();
|
|
74
|
+
for (const h of heads) {
|
|
75
|
+
const v = h[1];
|
|
76
|
+
let i = h.index + h[0].length;
|
|
77
|
+
let items = null;
|
|
78
|
+
let afterIter;
|
|
79
|
+
if (text[i] === '[') {
|
|
80
|
+
const close = balanced(text, i, '[', ']');
|
|
81
|
+
if (close < 0) continue;
|
|
82
|
+
items = literalArray(text.slice(i + 1, close));
|
|
83
|
+
afterIter = close + 1;
|
|
84
|
+
} else {
|
|
85
|
+
const id = /^([A-Za-z_$][\w$]*)/.exec(text.slice(i));
|
|
86
|
+
if (!id) continue;
|
|
87
|
+
items = constArray(execText, id[1]);
|
|
88
|
+
afterIter = i + id[1].length;
|
|
89
|
+
}
|
|
90
|
+
const paren = /^\s*(?:as\s+const\s*)?\)\s*\{/.exec(text.slice(afterIter));
|
|
91
|
+
if (!paren) continue;
|
|
92
|
+
const bodyOpen = afterIter + paren[0].length - 1;
|
|
93
|
+
const bodyClose = balanced(text, bodyOpen, '{', '}');
|
|
94
|
+
if (bodyClose < 0 || !items) continue; // left in place; the unresolved scan below names it
|
|
95
|
+
const body = text.slice(bodyOpen + 1, bodyClose);
|
|
96
|
+
const iv = new RegExp(`\\$\\{\\s*${v.replace(/\$/g, '\\$')}\\s*\\}`, 'g');
|
|
97
|
+
const expanded = items.map((it) => body.replace(iv, it)).join('\n');
|
|
98
|
+
text = text.slice(0, h.index) + expanded + text.slice(bodyClose + 1);
|
|
99
|
+
}
|
|
100
|
+
// Same-file string constants.
|
|
101
|
+
text = text.replace(/\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g, (all, name) => constString(execText, name) ?? all);
|
|
102
|
+
// Whatever is still interpolated inside an RLS-relevant SQL template.
|
|
103
|
+
const unresolved = [];
|
|
104
|
+
for (const m of text.matchAll(/\$\{([^}]*)\}/g)) {
|
|
105
|
+
const start = text.lastIndexOf('`', m.index);
|
|
106
|
+
const end = text.indexOf('`', m.index);
|
|
107
|
+
const tpl = text.slice(start < 0 ? 0 : start, end < 0 ? text.length : end);
|
|
108
|
+
const classes = classesOf(tpl);
|
|
109
|
+
if (classes.length) unresolved.push({ expr: m[1].trim(), classes });
|
|
110
|
+
}
|
|
111
|
+
return { text, unresolved };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Adapted from evaluateMigration(): creates = tables up() creates, drops = tables down() drops (in order). */
|
|
115
|
+
function downReversal(creates, drops, sharedTableCounts = {}) {
|
|
116
|
+
const problems = []
|
|
117
|
+
const owned = creates.filter((t) => (sharedTableCounts[t] || 1) <= 1)
|
|
118
|
+
if (owned.length && !drops.length) { problems.push({ rule: 'DOWN_INCOMPLETE', tables: owned }); return problems }
|
|
119
|
+
const missing = owned.filter((t) => !drops.includes(t))
|
|
120
|
+
if (missing.length) problems.push({ rule: 'DOWN_INCOMPLETE', tables: missing })
|
|
121
|
+
const expected = [...owned].reverse().filter((t) => drops.includes(t))
|
|
122
|
+
const actual = drops.filter((t) => owned.includes(t))
|
|
123
|
+
if (expected.length > 1 && expected.join(',') !== actual.join(',')) problems.push({ rule: 'DOWN_ORDER', expected, actual })
|
|
124
|
+
return problems
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { LIT, ROUTE, balanced, literalArray, constArray, constString, classesOf, expandStatic, downReversal }
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// PROVENANCE — ported, not rewritten.
|
|
2
|
+
// source : finipe/scripts/ci/check-secrets-and-log-redaction.mjs, part (a) "no secret material in the tree"
|
|
3
|
+
// commit : finipe 4ac53b367b348d23d000629cb14e19633beb9613 (file last changed in ae240e49e)
|
|
4
|
+
// kept : lines 62-129 and 157-200 verbatim (patterns, .env credential-key segmentation, placeholder rules,
|
|
5
|
+
// binary-extension list, scanContentForSecrets, lineOf).
|
|
6
|
+
// changed: Finipe path allowlist + git/fs walker removed (runner owns file listing; allowlisting is the
|
|
7
|
+
// customer's .fini-proof.json `ignore`), and the pure scanner is exported. Part (b), log redaction,
|
|
8
|
+
// is NestJS-specific (backend/src logger helpers) and is NOT ported in v0.1.
|
|
9
|
+
|
|
10
|
+
// ── (a) secret-material patterns ────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const AWS_ACCESS_KEY_RE = /\bAKIA[0-9A-Z]{16}\b/g
|
|
13
|
+
const PRIVATE_KEY_BLOCK_RE = /-----BEGIN (?:RSA |EC |OPENSSH |DSA |ENCRYPTED )?PRIVATE KEY-----/g
|
|
14
|
+
const JWT_ASSIGNED_RE = /([A-Za-z_$][\w$]*)\s*[:=]\s*['"`](eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,})['"`]/g
|
|
15
|
+
const SLACK_TOKEN_RE = /\bxox[baprs]-[0-9A-Za-z-]{10,}\b/g
|
|
16
|
+
const STRIPE_KEY_RE = /\b(?:sk|rk)_live_[0-9A-Za-z]{10,}\b/g
|
|
17
|
+
const SENDGRID_KEY_RE = /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g
|
|
18
|
+
// Any KEY=VALUE line in an .env* file — filtered below to those whose key
|
|
19
|
+
// has PASSWORD/SECRET/TOKEN as a whole UNDERSCORE-DELIMITED SEGMENT (never
|
|
20
|
+
// a plain substring: `_` is a \w character, so a regex `\bPASSWORD\b` does
|
|
21
|
+
// NOT see a boundary between `DB_` and `PASSWORD` and would silently match
|
|
22
|
+
// nothing — segment-splitting is used instead of `\b` for exactly this
|
|
23
|
+
// reason). `TOKENS` (plural) is therefore excluded automatically: it is
|
|
24
|
+
// never its own segment. A second pass drops keys that are self-evidently a
|
|
25
|
+
// numeric limit/toggle (`_CAP`, `_LIMIT`, `_MAX`, `_ENABLED`, …) despite
|
|
26
|
+
// containing one of those words as a segment, e.g.
|
|
27
|
+
// `COPILOT_MONTHLY_TOKEN_CAP_PER_TENANT`.
|
|
28
|
+
// NOTE: the whitespace around `=` is deliberately `[ \t]*`, never `\s*` —
|
|
29
|
+
// `\s` matches `\n`, and a greedy `\s*` immediately before a `(.*)` capture
|
|
30
|
+
// will happily cross the newline and swallow the ENTIRE NEXT LINE as this
|
|
31
|
+
// line's "value" on backtracking (caught by this gate's own self-test: an
|
|
32
|
+
// empty `KEY=` line was mis-captured as holding the next line's full text).
|
|
33
|
+
const ENV_ASSIGNMENT_RE = /^[ \t]*([A-Za-z_][A-Za-z0-9_]*)[ \t]*=(.*)$/gim
|
|
34
|
+
const ENV_CREDENTIAL_SEGMENTS = new Set(['PASSWORD', 'SECRET', 'TOKEN'])
|
|
35
|
+
const ENV_NON_CREDENTIAL_SEGMENTS = new Set(['CAP', 'LIMIT', 'MAX', 'COUNT', 'SIZE', 'TTL', 'EXPIRY', 'EXPIRES', 'DURATION', 'ENABLED', 'DISABLED'])
|
|
36
|
+
|
|
37
|
+
function envKeyLooksLikeCredential(key) {
|
|
38
|
+
const segments = key.toUpperCase().split('_')
|
|
39
|
+
if (!segments.some(s => ENV_CREDENTIAL_SEGMENTS.has(s))) return false
|
|
40
|
+
if (segments.some(s => ENV_NON_CREDENTIAL_SEGMENTS.has(s))) return false
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Substrings that are self-evidently placeholder, not real secret material —
|
|
45
|
+
// deliberately broad, as a substring test anywhere in the value. A security
|
|
46
|
+
// gate's false positives get reviewed and allowlisted or fixed here; its
|
|
47
|
+
// false NEGATIVES ship a real secret, which is the worse failure direction.
|
|
48
|
+
const PLACEHOLDER_SUBSTRINGS = [
|
|
49
|
+
'change-me', 'changeme', 'change_me', 'replace', 'your-', 'your_',
|
|
50
|
+
'todo', 'placeholder', 'example', 'sample', 'dummy', 'fake', 'stub',
|
|
51
|
+
'not-set', 'not_set', '<', '>', '...',
|
|
52
|
+
]
|
|
53
|
+
const PLACEHOLDER_EXACT_RE = /^["']?(?:|none|null|undefined|true|false|dev|development|staging|local|test)["']?$/i
|
|
54
|
+
// Inline env-file comment (`KEY= # explanatory text`, or `KEY=# text` once
|
|
55
|
+
// the assignment regex has already eaten the leading whitespace) is not a
|
|
56
|
+
// value.
|
|
57
|
+
const ENV_INLINE_COMMENT_RE = /(?:^|\s)#.*$/
|
|
58
|
+
// A value that is one character repeated (`0000…0`, `xxxxxxxx`) end to end.
|
|
59
|
+
const REPEATED_CHAR_VALUE_RE = /^(.)\1{5,}$/
|
|
60
|
+
// Universally-known, publicly-documented default credentials for local
|
|
61
|
+
// dev-only services — not secrets by any definition (anyone can look them
|
|
62
|
+
// up in the tool's own docs); flagging them is pure noise.
|
|
63
|
+
const KNOWN_DEV_DEFAULT_VALUES = new Set(['minioadmin', 'postgres', 'guest'])
|
|
64
|
+
|
|
65
|
+
function isPlaceholderEnvValue(rawValue) {
|
|
66
|
+
const v = rawValue.replace(ENV_INLINE_COMMENT_RE, '').trim()
|
|
67
|
+
if (!v) return true
|
|
68
|
+
const unquoted = v.replace(/^["']|["']$/g, '')
|
|
69
|
+
if (PLACEHOLDER_EXACT_RE.test(v)) return true
|
|
70
|
+
if (KNOWN_DEV_DEFAULT_VALUES.has(unquoted.toLowerCase())) return true
|
|
71
|
+
if (REPEATED_CHAR_VALUE_RE.test(unquoted)) return true
|
|
72
|
+
const lower = v.toLowerCase()
|
|
73
|
+
return PLACEHOLDER_SUBSTRINGS.some(s => lower.includes(s))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Allowlisted EXACTLY by path — fixtures/test vectors that legitimately embed
|
|
77
|
+
// fake-shaped secret material to exercise the code that handles real ones.
|
|
78
|
+
const BINARY_EXT_RE = /\.(png|jpg|jpeg|gif|webp|ico|pdf|woff2?|ttf|eot|zip|gz|tgz|jar|class|so|dylib|dll|exe|bin|wasm|mp4|mov|mp3|db|sqlite)$/i
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Scan one file's CONTENT (already read) for secret material. Pure function —
|
|
82
|
+
* no fs/git access — so the self-test below can drive it directly on fixture
|
|
83
|
+
* strings without needing the fixture to be `git`-tracked.
|
|
84
|
+
*/
|
|
85
|
+
function scanContentForSecrets(relPath, content) {
|
|
86
|
+
const findings = []
|
|
87
|
+
const isEnvFile = /(^|\/)\.env(\..+)?$/.test(relPath)
|
|
88
|
+
|
|
89
|
+
for (const re of [AWS_ACCESS_KEY_RE, PRIVATE_KEY_BLOCK_RE, SLACK_TOKEN_RE, STRIPE_KEY_RE, SENDGRID_KEY_RE]) {
|
|
90
|
+
re.lastIndex = 0
|
|
91
|
+
let m
|
|
92
|
+
while ((m = re.exec(content)) !== null) {
|
|
93
|
+
findings.push({ kind: re === AWS_ACCESS_KEY_RE ? 'aws-access-key' : re === PRIVATE_KEY_BLOCK_RE ? 'private-key-block' : re === SLACK_TOKEN_RE ? 'slack-token' : re === STRIPE_KEY_RE ? 'stripe-live-key' : 'sendgrid-key', line: lineOf(content, m.index), match: m[0].slice(0, 24) + (m[0].length > 24 ? '…' : '') })
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
JWT_ASSIGNED_RE.lastIndex = 0
|
|
99
|
+
let m
|
|
100
|
+
while ((m = JWT_ASSIGNED_RE.exec(content)) !== null) {
|
|
101
|
+
findings.push({ kind: 'jwt-assigned-to-config', line: lineOf(content, m.index), match: `${m[1]} = ${m[2].slice(0, 20)}…` })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (isEnvFile) {
|
|
106
|
+
ENV_ASSIGNMENT_RE.lastIndex = 0
|
|
107
|
+
let m
|
|
108
|
+
while ((m = ENV_ASSIGNMENT_RE.exec(content)) !== null) {
|
|
109
|
+
const key = m[1]
|
|
110
|
+
if (!envKeyLooksLikeCredential(key)) continue
|
|
111
|
+
if (isPlaceholderEnvValue(m[2])) continue
|
|
112
|
+
findings.push({ kind: 'env-secret-assignment', line: lineOf(content, m.index), match: `${key}=<redacted-for-report>` })
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return findings
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function lineOf(src, idx) {
|
|
120
|
+
return src.slice(0, idx).split('\n').length
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { scanContentForSecrets, BINARY_EXT_RE, lineOf }
|