flecto 3.0.1 → 3.1.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/CHANGELOG.md +567 -1
- package/README.md +15 -2
- package/index.js +544 -154
- package/package.json +4 -1
- package/schemas/flecto-policy-pack-2.0.json +2 -0
- package/src/baseline.js +193 -0
- package/src/config.js +564 -6
- package/src/differ.js +76 -12
- package/src/encrypted.js +16 -13
- package/src/packs/github-actions.json +92 -0
- package/src/parser.js +212 -22
- package/src/policy-test.js +5 -1
- package/src/policy.js +96 -23
- package/src/pr-comment.js +95 -87
- package/src/pr-providers.js +268 -0
- package/src/renderer.js +10 -22
- package/src/report.js +43 -3
- package/src/sarif.js +144 -0
- package/src/secrets.js +68 -7
- package/src/snapshot-store.js +668 -0
- package/src/suppressions.js +431 -0
- package/src/terraform.js +28 -0
package/src/secrets.js
CHANGED
|
@@ -57,15 +57,22 @@ const KNOWN_FORMATS = [
|
|
|
57
57
|
{ kind: 'stripe-secret-key', re: /\b[sr]k_live_[0-9A-Za-z]{16,}\b/g },
|
|
58
58
|
// JWT: base64url header starting with "eyJ" ('{"'), payload, signature.
|
|
59
59
|
{ kind: 'jwt', re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g },
|
|
60
|
-
// PEM private key blocks, including PGP blocks and unterminated fragments.
|
|
61
|
-
{
|
|
62
|
-
kind: 'private-key-block',
|
|
63
|
-
re: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|$)/g,
|
|
64
|
-
},
|
|
65
60
|
];
|
|
66
61
|
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
// PEM/PGP private-key block markers, matched linearly. A single regex spanning
|
|
63
|
+
// BEGIN…END with `[\s\S]*?…$` backtracks quadratically on a long BEGIN-prefixed
|
|
64
|
+
// value with no END — a denial-of-service vector, since secret detection runs
|
|
65
|
+
// on every changed string value. Instead we find the markers with anchored,
|
|
66
|
+
// non-spanning regexes and pair them by position (see findPrivateKeyBlocks).
|
|
67
|
+
const PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
68
|
+
const PRIVATE_KEY_END_RE = /-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
69
|
+
|
|
70
|
+
// Credentials embedded in a URL authority: scheme://user:PASSWORD@host. The
|
|
71
|
+
// scheme run is length-bounded ({0,32}); an unbounded `*` before the required
|
|
72
|
+
// `://` backtracks quadratically on a long value that never contains `://`. No
|
|
73
|
+
// real URL scheme approaches 32 characters, so the bound changes nothing that
|
|
74
|
+
// matters and removes the ReDoS.
|
|
75
|
+
const URL_CREDENTIALS_RE = /[a-z][a-z0-9+.-]{0,32}:\/\/[^\s/:@]+:([^\s/@]+)@/gi;
|
|
69
76
|
|
|
70
77
|
/**
|
|
71
78
|
* Values that only *reference* a secret. Redacting these adds noise and, worse,
|
|
@@ -205,6 +212,31 @@ function isHighEntropySecret(value) {
|
|
|
205
212
|
return true;
|
|
206
213
|
}
|
|
207
214
|
|
|
215
|
+
/**
|
|
216
|
+
* PEM/PGP private-key spans, found linearly. Each BEGIN marker pairs with the
|
|
217
|
+
* next END marker after it; a BEGIN with no following END runs to end of string
|
|
218
|
+
* (an unterminated fragment is still a leaked key). Pairing by position avoids
|
|
219
|
+
* the quadratic backtracking a single BEGIN…END regex incurs.
|
|
220
|
+
* @param {string} value
|
|
221
|
+
* @returns {SecretMatch[]}
|
|
222
|
+
*/
|
|
223
|
+
function findPrivateKeyBlocks(value) {
|
|
224
|
+
/** @type {SecretMatch[]} */
|
|
225
|
+
const spans = [];
|
|
226
|
+
PRIVATE_KEY_BEGIN_RE.lastIndex = 0;
|
|
227
|
+
let begin;
|
|
228
|
+
while ((begin = PRIVATE_KEY_BEGIN_RE.exec(value)) !== null) {
|
|
229
|
+
const bodyStart = begin.index + begin[0].length;
|
|
230
|
+
PRIVATE_KEY_END_RE.lastIndex = bodyStart;
|
|
231
|
+
const end = PRIVATE_KEY_END_RE.exec(value);
|
|
232
|
+
const spanEnd = end ? end.index + end[0].length : value.length;
|
|
233
|
+
spans.push({ kind: 'private-key-block', start: begin.index, end: spanEnd });
|
|
234
|
+
// Resume scanning past this block so overlapping BEGINs inside it are skipped.
|
|
235
|
+
PRIVATE_KEY_BEGIN_RE.lastIndex = spanEnd;
|
|
236
|
+
}
|
|
237
|
+
return spans;
|
|
238
|
+
}
|
|
239
|
+
|
|
208
240
|
/**
|
|
209
241
|
* Locate every secret-shaped span inside a string, sorted and non-overlapping.
|
|
210
242
|
* @param {string} value
|
|
@@ -223,6 +255,8 @@ function findSecretMatches(value) {
|
|
|
223
255
|
}
|
|
224
256
|
}
|
|
225
257
|
|
|
258
|
+
matches.push(...findPrivateKeyBlocks(value));
|
|
259
|
+
|
|
226
260
|
URL_CREDENTIALS_RE.lastIndex = 0;
|
|
227
261
|
let credentials;
|
|
228
262
|
while ((credentials = URL_CREDENTIALS_RE.exec(value)) !== null) {
|
|
@@ -296,6 +330,33 @@ export function redactSecretString(value) {
|
|
|
296
330
|
return out + value.slice(cursor);
|
|
297
331
|
}
|
|
298
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Key names that mean "this value is a credential".
|
|
335
|
+
*
|
|
336
|
+
* Matched against the *configuration* path only. A multi-document file prefixes
|
|
337
|
+
* every path with the document's identity — `Deployment/prod/token-service.…` —
|
|
338
|
+
* and that identity is a resource name the user chose, not a key name. Letting
|
|
339
|
+
* it match masks every value in the document, numbers and booleans included, so
|
|
340
|
+
* callers strip the document prefix before matching: see `secretMatchPath` in
|
|
341
|
+
* differ.js, and the document handling in `maskState`.
|
|
342
|
+
*/
|
|
343
|
+
export const SECRET_PATH_RE = /(secret|token|password|api[_-]?key|private[_-]?key|credential)/i;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* True when a configuration path names a credential.
|
|
347
|
+
*
|
|
348
|
+
* Lives here rather than beside either caller because there are two of them —
|
|
349
|
+
* the renderer masking a diff for display, and the snapshot store masking a
|
|
350
|
+
* state for a commit — and a store that recognized fewer key names than the
|
|
351
|
+
* terminal did would write into git history exactly the values the terminal
|
|
352
|
+
* thought were too sensitive to print.
|
|
353
|
+
* @param {string} path
|
|
354
|
+
* @returns {boolean}
|
|
355
|
+
*/
|
|
356
|
+
export function looksLikeSecretPath(path) {
|
|
357
|
+
return SECRET_PATH_RE.test(path);
|
|
358
|
+
}
|
|
359
|
+
|
|
299
360
|
/**
|
|
300
361
|
* True when a value — or any string nested inside a plain object or array —
|
|
301
362
|
* looks like a secret.
|