@grafana/plugin-ui 0.17.3 → 0.17.4
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/NOTICE +18 -0
- package/dist/cjs/index.cjs +3 -3
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/secret-scanner.cjs +3032 -0
- package/dist/cjs/secret-scanner.cjs.map +1 -0
- package/dist/cjs/secret-scanner.d.cts +273 -0
- package/dist/esm/node_modules/js-yaml/dist/js-yaml.js +3 -3
- package/dist/esm/node_modules/js-yaml/dist/js-yaml.js.map +1 -1
- package/dist/esm/secret-scanner/core/constants.js +4 -0
- package/dist/esm/secret-scanner/core/constants.js.map +1 -0
- package/dist/esm/secret-scanner/core/detection/embedded.js +80 -0
- package/dist/esm/secret-scanner/core/detection/embedded.js.map +1 -0
- package/dist/esm/secret-scanner/core/detection/entropy.js +19 -0
- package/dist/esm/secret-scanner/core/detection/entropy.js.map +1 -0
- package/dist/esm/secret-scanner/core/detection/highEntropy.js +24 -0
- package/dist/esm/secret-scanner/core/detection/highEntropy.js.map +1 -0
- package/dist/esm/secret-scanner/core/detection/literals.js +135 -0
- package/dist/esm/secret-scanner/core/detection/literals.js.map +1 -0
- package/dist/esm/secret-scanner/core/detection/nameDetector.js +48 -0
- package/dist/esm/secret-scanner/core/detection/nameDetector.js.map +1 -0
- package/dist/esm/secret-scanner/core/detection/placeholders.js +10 -0
- package/dist/esm/secret-scanner/core/detection/placeholders.js.map +1 -0
- package/dist/esm/secret-scanner/core/detection/position.js +39 -0
- package/dist/esm/secret-scanner/core/detection/position.js.map +1 -0
- package/dist/esm/secret-scanner/core/detection/ruleDetectors.js +59 -0
- package/dist/esm/secret-scanner/core/detection/ruleDetectors.js.map +1 -0
- package/dist/esm/secret-scanner/core/k6Rewriter.js +44 -0
- package/dist/esm/secret-scanner/core/k6Rewriter.js.map +1 -0
- package/dist/esm/secret-scanner/core/memoizeLatest.js +18 -0
- package/dist/esm/secret-scanner/core/memoizeLatest.js.map +1 -0
- package/dist/esm/secret-scanner/core/scanCode.js +70 -0
- package/dist/esm/secret-scanner/core/scanCode.js.map +1 -0
- package/dist/esm/secret-scanner/core/scanText.js +51 -0
- package/dist/esm/secret-scanner/core/scanText.js.map +1 -0
- package/dist/esm/secret-scanner/core/secretScanner.utils.js +33 -0
- package/dist/esm/secret-scanner/core/secretScanner.utils.js.map +1 -0
- package/dist/esm/secret-scanner/monaco/installSecretScanner.js +117 -0
- package/dist/esm/secret-scanner/monaco/installSecretScanner.js.map +1 -0
- package/dist/esm/secret-scanner/panel/SecretReferenceModal.js +21 -0
- package/dist/esm/secret-scanner/panel/SecretReferenceModal.js.map +1 -0
- package/dist/esm/secret-scanner/panel/SecretScannerPanel.js +75 -0
- package/dist/esm/secret-scanner/panel/SecretScannerPanel.js.map +1 -0
- package/dist/esm/secret-scanner/react/useIgnoredSecrets.js +25 -0
- package/dist/esm/secret-scanner/react/useIgnoredSecrets.js.map +1 -0
- package/dist/esm/secret-scanner/react/useLocalStorageList.js +38 -0
- package/dist/esm/secret-scanner/react/useLocalStorageList.js.map +1 -0
- package/dist/esm/secret-scanner/react/useMonacoSecretScanner.js +28 -0
- package/dist/esm/secret-scanner/react/useMonacoSecretScanner.js.map +1 -0
- package/dist/esm/secret-scanner/react/useSecretScanner.js +57 -0
- package/dist/esm/secret-scanner/react/useSecretScanner.js.map +1 -0
- package/dist/esm/secret-scanner/rules/rules.generated.js +2083 -0
- package/dist/esm/secret-scanner/rules/rules.generated.js.map +1 -0
- package/dist/esm/secret-scanner.d.ts +273 -0
- package/dist/esm/secret-scanner.js +15 -0
- package/dist/esm/secret-scanner.js.map +1 -0
- package/package.json +37 -3
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { iterateStringLiterals } from './detection/literals.js';
|
|
2
|
+
import { isPlaceholder } from './detection/placeholders.js';
|
|
3
|
+
import { createPositionResolver } from './detection/position.js';
|
|
4
|
+
export { offsetToPosition } from './detection/position.js';
|
|
5
|
+
import { detectEmbedded } from './detection/embedded.js';
|
|
6
|
+
import { detectHighEntropy } from './detection/highEntropy.js';
|
|
7
|
+
import { detectFromName } from './detection/nameDetector.js';
|
|
8
|
+
import { memoizeLatest } from './memoizeLatest.js';
|
|
9
|
+
|
|
10
|
+
function scan$1(script) {
|
|
11
|
+
var _a;
|
|
12
|
+
const findings = [];
|
|
13
|
+
const positionAt = createPositionResolver(script);
|
|
14
|
+
for (const {
|
|
15
|
+
body,
|
|
16
|
+
literalStart,
|
|
17
|
+
literalEnd,
|
|
18
|
+
contentStart,
|
|
19
|
+
contentEnd,
|
|
20
|
+
kind,
|
|
21
|
+
name,
|
|
22
|
+
embeddedOnly
|
|
23
|
+
} of iterateStringLiterals(script)) {
|
|
24
|
+
const literal = { start: literalStart, end: literalEnd };
|
|
25
|
+
const embedded = detectEmbedded(script.slice(contentStart, contentEnd));
|
|
26
|
+
if (embedded) {
|
|
27
|
+
const spansWholeLiteral = !embeddedOnly && embedded.index === 0 && embedded.length === contentEnd - contentStart;
|
|
28
|
+
const secretStart = contentStart + embedded.index;
|
|
29
|
+
const range = { start: secretStart, end: secretStart + embedded.length };
|
|
30
|
+
const { line, column } = positionAt(range.start);
|
|
31
|
+
findings.push({
|
|
32
|
+
id: `${range.start}-${embedded.type}`,
|
|
33
|
+
type: embedded.type,
|
|
34
|
+
label: embedded.label,
|
|
35
|
+
confidence: embedded.confidence,
|
|
36
|
+
secret: embedded.secret,
|
|
37
|
+
range,
|
|
38
|
+
line,
|
|
39
|
+
column,
|
|
40
|
+
literal,
|
|
41
|
+
rewrite: spansWholeLiteral ? "literal" : kind === "template" ? "template-interpolation" : "none"
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!embeddedOnly && body.length > 0 && !isPlaceholder(body)) {
|
|
46
|
+
const detected = (_a = detectHighEntropy(body)) != null ? _a : detectFromName(body, name);
|
|
47
|
+
if (detected) {
|
|
48
|
+
const range = { start: contentStart, end: contentEnd };
|
|
49
|
+
const { line, column } = positionAt(range.start);
|
|
50
|
+
findings.push({
|
|
51
|
+
id: `${range.start}-${detected.type}`,
|
|
52
|
+
...detected,
|
|
53
|
+
range,
|
|
54
|
+
line,
|
|
55
|
+
column,
|
|
56
|
+
literal,
|
|
57
|
+
rewrite: "literal"
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return findings;
|
|
63
|
+
}
|
|
64
|
+
function createCodeScanner() {
|
|
65
|
+
return memoizeLatest(scan$1);
|
|
66
|
+
}
|
|
67
|
+
const scanCode = createCodeScanner();
|
|
68
|
+
|
|
69
|
+
export { createCodeScanner, scanCode };
|
|
70
|
+
//# sourceMappingURL=scanCode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanCode.js","sources":["../../../../src/secret-scanner/core/scanCode.ts"],"sourcesContent":["/**\n * Code-first scanner: flags likely hardcoded secrets in a script.\n *\n * String-literal aware (AST-based) rather than raw-text, so reported ranges line\n * up with what the user sees and a rewrite can edit the original text.\n *\n * For plain-text outside a literal, use `scanText`.\n *\n * Orchestration only; the detectors live in `./detection`.\n */\n\nimport { iterateStringLiterals } from './detection/literals';\nimport { isPlaceholder } from './detection/placeholders';\nimport { createPositionResolver } from './detection/position';\nimport { detectEmbedded } from './detection/embedded';\nimport { detectHighEntropy } from './detection/highEntropy';\nimport { detectFromName } from './detection/nameDetector';\nimport type { SecretFinding } from './detection/types';\nimport { memoizeLatest } from './memoizeLatest';\n\nfunction scan(script: string): SecretFinding[] {\n const findings: SecretFinding[] = [];\n const positionAt = createPositionResolver(script);\n\n for (const {\n body,\n literalStart,\n literalEnd,\n contentStart,\n contentEnd,\n kind,\n name,\n embeddedOnly,\n } of iterateStringLiterals(script)) {\n const literal = { start: literalStart, end: literalEnd };\n\n const embedded = detectEmbedded(script.slice(contentStart, contentEnd));\n if (embedded) {\n // A match covering the whole content means the literal *is* the secret\n const spansWholeLiteral = !embeddedOnly && embedded.index === 0 && embedded.length === contentEnd - contentStart;\n const secretStart = contentStart + embedded.index;\n const range = { start: secretStart, end: secretStart + embedded.length };\n const { line, column } = positionAt(range.start);\n\n findings.push({\n id: `${range.start}-${embedded.type}`,\n type: embedded.type,\n label: embedded.label,\n confidence: embedded.confidence,\n secret: embedded.secret,\n range,\n line,\n column,\n literal,\n rewrite: spansWholeLiteral ? 'literal' : kind === 'template' ? 'template-interpolation' : 'none',\n });\n continue;\n }\n\n if (!embeddedOnly && body.length > 0 && !isPlaceholder(body)) {\n const detected = detectHighEntropy(body) ?? detectFromName(body, name);\n if (detected) {\n const range = { start: contentStart, end: contentEnd };\n const { line, column } = positionAt(range.start);\n findings.push({\n id: `${range.start}-${detected.type}`,\n ...detected,\n range,\n line,\n column,\n literal,\n rewrite: 'literal',\n });\n }\n }\n }\n\n return findings;\n}\n\n/**\n * A code scanner with its own memo. Prefer this to the shared {@link scanCode}\n * when more than one surface scans independently — see `memoizeLatest`.\n */\nexport function createCodeScanner(): (script: string) => SecretFinding[] {\n return memoizeLatest(scan);\n}\n\n/** Shared code scanner, for a host with a single scanning surface. */\nexport const scanCode = createCodeScanner();\n\nexport { offsetToPosition } from './detection/position';\nexport { shannonEntropy } from './detection/entropy';\nexport type { SecretFinding, SecretConfidence, SecretRange, SecretRewrite } from './detection/types';\n"],"names":["scan"],"mappings":";;;;;;;;;AAoBA,SAASA,OAAK,MAAA,EAAiC;AApB/C,EAAA,IAAA,EAAA;AAqBE,EAAA,MAAM,WAA4B,EAAC;AACnC,EAAA,MAAM,UAAA,GAAa,uBAAuB,MAAM,CAAA;AAEhD,EAAA,KAAA,MAAW;AAAA,IACT,IAAA;AAAA,IACA,YAAA;AAAA,IACA,UAAA;AAAA,IACA,YAAA;AAAA,IACA,UAAA;AAAA,IACA,IAAA;AAAA,IACA,IAAA;AAAA,IACA;AAAA,GACF,IAAK,qBAAA,CAAsB,MAAM,CAAA,EAAG;AAClC,IAAA,MAAM,OAAA,GAAU,EAAE,KAAA,EAAO,YAAA,EAAc,KAAK,UAAA,EAAW;AAEvD,IAAA,MAAM,WAAW,cAAA,CAAe,MAAA,CAAO,KAAA,CAAM,YAAA,EAAc,UAAU,CAAC,CAAA;AACtE,IAAA,IAAI,QAAA,EAAU;AAEZ,MAAA,MAAM,iBAAA,GAAoB,CAAC,YAAA,IAAgB,QAAA,CAAS,UAAU,CAAA,IAAK,QAAA,CAAS,WAAW,UAAA,GAAa,YAAA;AACpG,MAAA,MAAM,WAAA,GAAc,eAAe,QAAA,CAAS,KAAA;AAC5C,MAAA,MAAM,QAAQ,EAAE,KAAA,EAAO,aAAa,GAAA,EAAK,WAAA,GAAc,SAAS,MAAA,EAAO;AACvE,MAAA,MAAM,EAAE,IAAA,EAAM,MAAA,EAAO,GAAI,UAAA,CAAW,MAAM,KAAK,CAAA;AAE/C,MAAA,QAAA,CAAS,IAAA,CAAK;AAAA,QACZ,IAAI,CAAA,EAAG,KAAA,CAAM,KAAK,CAAA,CAAA,EAAI,SAAS,IAAI,CAAA,CAAA;AAAA,QACnC,MAAM,QAAA,CAAS,IAAA;AAAA,QACf,OAAO,QAAA,CAAS,KAAA;AAAA,QAChB,YAAY,QAAA,CAAS,UAAA;AAAA,QACrB,QAAQ,QAAA,CAAS,MAAA;AAAA,QACjB,KAAA;AAAA,QACA,IAAA;AAAA,QACA,MAAA;AAAA,QACA,OAAA;AAAA,QACA,OAAA,EAAS,iBAAA,GAAoB,SAAA,GAAY,IAAA,KAAS,aAAa,wBAAA,GAA2B;AAAA,OAC3F,CAAA;AACD,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,gBAAgB,IAAA,CAAK,MAAA,GAAS,KAAK,CAAC,aAAA,CAAc,IAAI,CAAA,EAAG;AAC5D,MAAA,MAAM,YAAW,EAAA,GAAA,iBAAA,CAAkB,IAAI,MAAtB,IAAA,GAAA,EAAA,GAA2B,cAAA,CAAe,MAAM,IAAI,CAAA;AACrE,MAAA,IAAI,QAAA,EAAU;AACZ,QAAA,MAAM,KAAA,GAAQ,EAAE,KAAA,EAAO,YAAA,EAAc,KAAK,UAAA,EAAW;AACrD,QAAA,MAAM,EAAE,IAAA,EAAM,MAAA,EAAO,GAAI,UAAA,CAAW,MAAM,KAAK,CAAA;AAC/C,QAAA,QAAA,CAAS,IAAA,CAAK;AAAA,UACZ,IAAI,CAAA,EAAG,KAAA,CAAM,KAAK,CAAA,CAAA,EAAI,SAAS,IAAI,CAAA,CAAA;AAAA,UACnC,GAAG,QAAA;AAAA,UACH,KAAA;AAAA,UACA,IAAA;AAAA,UACA,MAAA;AAAA,UACA,OAAA;AAAA,UACA,OAAA,EAAS;AAAA,SACV,CAAA;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,QAAA;AACT;AAMO,SAAS,iBAAA,GAAyD;AACvE,EAAA,OAAO,cAAcA,MAAI,CAAA;AAC3B;AAGO,MAAM,WAAW,iBAAA;;;;"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { findAllEmbedded } from './detection/embedded.js';
|
|
2
|
+
import { detectHighEntropy } from './detection/highEntropy.js';
|
|
3
|
+
import { isPlaceholder } from './detection/placeholders.js';
|
|
4
|
+
import { createPositionResolver } from './detection/position.js';
|
|
5
|
+
import { memoizeLatest } from './memoizeLatest.js';
|
|
6
|
+
|
|
7
|
+
const HIGH_ENTROPY_TOKEN = /[A-Za-z0-9+/=]{24,}/g;
|
|
8
|
+
function highEntropyMatches(text) {
|
|
9
|
+
const matches = [];
|
|
10
|
+
HIGH_ENTROPY_TOKEN.lastIndex = 0;
|
|
11
|
+
let m;
|
|
12
|
+
while ((m = HIGH_ENTROPY_TOKEN.exec(text)) !== null) {
|
|
13
|
+
const token = m[0];
|
|
14
|
+
if (isPlaceholder(token)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const detected = detectHighEntropy(token);
|
|
18
|
+
if (detected) {
|
|
19
|
+
matches.push({ ...detected, index: m.index, length: token.length });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return matches;
|
|
23
|
+
}
|
|
24
|
+
function scan(text) {
|
|
25
|
+
const ruleMatches = findAllEmbedded(text);
|
|
26
|
+
const overlapsRuleMatch = (m) => ruleMatches.some((rule) => m.index < rule.index + rule.length && rule.index < m.index + m.length);
|
|
27
|
+
const matches = [...ruleMatches, ...highEntropyMatches(text).filter((m) => !overlapsRuleMatch(m))].sort(
|
|
28
|
+
(a, b) => a.index - b.index
|
|
29
|
+
);
|
|
30
|
+
const positionAt = createPositionResolver(text);
|
|
31
|
+
return matches.map((match) => {
|
|
32
|
+
const { line, column } = positionAt(match.index);
|
|
33
|
+
return {
|
|
34
|
+
id: `${match.index}-${match.type}`,
|
|
35
|
+
type: match.type,
|
|
36
|
+
label: match.label,
|
|
37
|
+
confidence: match.confidence,
|
|
38
|
+
secret: match.secret,
|
|
39
|
+
range: { start: match.index, end: match.index + match.length },
|
|
40
|
+
line,
|
|
41
|
+
column
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function createTextScanner() {
|
|
46
|
+
return memoizeLatest(scan);
|
|
47
|
+
}
|
|
48
|
+
const scanText = createTextScanner();
|
|
49
|
+
|
|
50
|
+
export { createTextScanner, scanText };
|
|
51
|
+
//# sourceMappingURL=scanText.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanText.js","sources":["../../../../src/secret-scanner/core/scanText.ts"],"sourcesContent":["/**\n * Text-first counterpart to {@link scanCode}: runs the same value-shape\n * detectors over raw text rather than over parsed string literals, which is what\n * a natural-language surface (a prompt, a description) needs — there the\n * credential is typed inline, not quoted as a JS string.\n *\n */\nimport { type EmbeddedMatch, findAllEmbedded } from './detection/embedded';\nimport { detectHighEntropy } from './detection/highEntropy';\nimport { isPlaceholder } from './detection/placeholders';\nimport { createPositionResolver } from './detection/position';\nimport type { SecretFinding } from './detection/types';\nimport { memoizeLatest } from './memoizeLatest';\n\n// A run of high-entropy-alphabet characters, long enough to be a secret blob.\n// The alphabet mirrors `detectHighEntropy` (no `-`/`_`, so kebab/snake config\n// and identifiers aren't split into candidates), and the quotes/whitespace/\n// punctuation around a token in prose delimit it.\nconst HIGH_ENTROPY_TOKEN = /[A-Za-z0-9+/=]{24,}/g;\n\nfunction highEntropyMatches(text: string): EmbeddedMatch[] {\n const matches: EmbeddedMatch[] = [];\n HIGH_ENTROPY_TOKEN.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = HIGH_ENTROPY_TOKEN.exec(text)) !== null) {\n const token = m[0];\n if (isPlaceholder(token)) {\n continue;\n }\n const detected = detectHighEntropy(token);\n if (detected) {\n matches.push({ ...detected, index: m.index, length: token.length });\n }\n }\n return matches;\n}\n\nfunction scan(text: string): SecretFinding[] {\n const ruleMatches = findAllEmbedded(text);\n const overlapsRuleMatch = (m: EmbeddedMatch) =>\n ruleMatches.some((rule) => m.index < rule.index + rule.length && rule.index < m.index + m.length);\n\n const matches = [...ruleMatches, ...highEntropyMatches(text).filter((m) => !overlapsRuleMatch(m))].sort(\n (a, b) => a.index - b.index\n );\n\n const positionAt = createPositionResolver(text);\n return matches.map((match): SecretFinding => {\n const { line, column } = positionAt(match.index);\n return {\n id: `${match.index}-${match.type}`,\n type: match.type,\n label: match.label,\n confidence: match.confidence,\n secret: match.secret,\n range: { start: match.index, end: match.index + match.length },\n line,\n column,\n };\n });\n}\n\n/**\n * A text scanner with its own memo. Prefer this to the shared {@link scanText}\n * when more than one surface scans independently — see `memoizeLatest`.\n */\nexport function createTextScanner(): (text: string) => SecretFinding[] {\n return memoizeLatest(scan);\n}\n\n/** Shared text scanner, for a host with a single scanning surface. */\nexport const scanText = createTextScanner();\n"],"names":[],"mappings":";;;;;;AAkBA,MAAM,kBAAA,GAAqB,sBAAA;AAE3B,SAAS,mBAAmB,IAAA,EAA+B;AACzD,EAAA,MAAM,UAA2B,EAAC;AAClC,EAAA,kBAAA,CAAmB,SAAA,GAAY,CAAA;AAC/B,EAAA,IAAI,CAAA;AACJ,EAAA,OAAA,CAAQ,CAAA,GAAI,kBAAA,CAAmB,IAAA,CAAK,IAAI,OAAO,IAAA,EAAM;AACnD,IAAA,MAAM,KAAA,GAAQ,EAAE,CAAC,CAAA;AACjB,IAAA,IAAI,aAAA,CAAc,KAAK,CAAA,EAAG;AACxB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,QAAA,GAAW,kBAAkB,KAAK,CAAA;AACxC,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,OAAA,CAAQ,IAAA,CAAK,EAAE,GAAG,QAAA,EAAU,KAAA,EAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,KAAA,CAAM,MAAA,EAAQ,CAAA;AAAA,IACpE;AAAA,EACF;AACA,EAAA,OAAO,OAAA;AACT;AAEA,SAAS,KAAK,IAAA,EAA+B;AAC3C,EAAA,MAAM,WAAA,GAAc,gBAAgB,IAAI,CAAA;AACxC,EAAA,MAAM,oBAAoB,CAAC,CAAA,KACzB,YAAY,IAAA,CAAK,CAAC,SAAS,CAAA,CAAE,KAAA,GAAQ,IAAA,CAAK,KAAA,GAAQ,KAAK,MAAA,IAAU,IAAA,CAAK,QAAQ,CAAA,CAAE,KAAA,GAAQ,EAAE,MAAM,CAAA;AAElG,EAAA,MAAM,UAAU,CAAC,GAAG,WAAA,EAAa,GAAG,mBAAmB,IAAI,CAAA,CAAE,MAAA,CAAO,CAAC,MAAM,CAAC,iBAAA,CAAkB,CAAC,CAAC,CAAC,CAAA,CAAE,IAAA;AAAA,IACjG,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,QAAQ,CAAA,CAAE;AAAA,GACxB;AAEA,EAAA,MAAM,UAAA,GAAa,uBAAuB,IAAI,CAAA;AAC9C,EAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,CAAC,KAAA,KAAyB;AAC3C,IAAA,MAAM,EAAE,IAAA,EAAM,MAAA,EAAO,GAAI,UAAA,CAAW,MAAM,KAAK,CAAA;AAC/C,IAAA,OAAO;AAAA,MACL,IAAI,CAAA,EAAG,KAAA,CAAM,KAAK,CAAA,CAAA,EAAI,MAAM,IAAI,CAAA,CAAA;AAAA,MAChC,MAAM,KAAA,CAAM,IAAA;AAAA,MACZ,OAAO,KAAA,CAAM,KAAA;AAAA,MACb,YAAY,KAAA,CAAM,UAAA;AAAA,MAClB,QAAQ,KAAA,CAAM,MAAA;AAAA,MACd,KAAA,EAAO,EAAE,KAAA,EAAO,KAAA,CAAM,OAAO,GAAA,EAAK,KAAA,CAAM,KAAA,GAAQ,KAAA,CAAM,MAAA,EAAO;AAAA,MAC7D,IAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF,CAAC,CAAA;AACH;AAMO,SAAS,iBAAA,GAAuD;AACrE,EAAA,OAAO,cAAc,IAAI,CAAA;AAC3B;AAGO,MAAM,WAAW,iBAAA;;;;"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const MAX_NAME_LENGTH = 253;
|
|
2
|
+
function suggestSecretName(finding, existingNames = []) {
|
|
3
|
+
const base = `${finding.type}-secret`.toLowerCase().replace(/[^a-z0-9.-]+/g, "-").replace(/^[^a-z0-9]+/, "").replace(/-+$/, "").slice(0, MAX_NAME_LENGTH);
|
|
4
|
+
const taken = new Set(existingNames);
|
|
5
|
+
if (!taken.has(base)) {
|
|
6
|
+
return base;
|
|
7
|
+
}
|
|
8
|
+
let suffix = 2;
|
|
9
|
+
while (taken.has(`${base}-${suffix}`)) {
|
|
10
|
+
suffix++;
|
|
11
|
+
}
|
|
12
|
+
return `${base}-${suffix}`;
|
|
13
|
+
}
|
|
14
|
+
function maskSecret(secret) {
|
|
15
|
+
if (secret.length <= 8) {
|
|
16
|
+
return "\u2022".repeat(secret.length);
|
|
17
|
+
}
|
|
18
|
+
return `${secret.slice(0, 3)}${"\u2022".repeat(4)}${secret.slice(-2)}`;
|
|
19
|
+
}
|
|
20
|
+
function getFindingSignature(finding) {
|
|
21
|
+
return `${finding.type}.${fnv1aHash(`${finding.type}:${finding.secret}`)}`;
|
|
22
|
+
}
|
|
23
|
+
function fnv1aHash(value) {
|
|
24
|
+
let hash = 2166136261;
|
|
25
|
+
for (let i = 0; i < value.length; i++) {
|
|
26
|
+
hash ^= value.charCodeAt(i);
|
|
27
|
+
hash = Math.imul(hash, 16777619);
|
|
28
|
+
}
|
|
29
|
+
return (hash >>> 0).toString(16);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { getFindingSignature, maskSecret, suggestSecretName };
|
|
33
|
+
//# sourceMappingURL=secretScanner.utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secretScanner.utils.js","sources":["../../../../src/secret-scanner/core/secretScanner.utils.ts"],"sourcesContent":["import type { SecretFinding } from './detection/types';\n\nconst MAX_NAME_LENGTH = 253;\n\n/**\n * Suggests a Secrets-store-compatible name for a finding. Secret names must be\n * lowercase and may only contain letters, numbers, dashes and periods (see\n * `secretSchema`), so we derive the suggestion from the detector type and\n * de-duplicate against existing names.\n */\nexport function suggestSecretName(finding: Pick<SecretFinding, 'type'>, existingNames: string[] = []): string {\n const base = `${finding.type}-secret`\n .toLowerCase()\n .replace(/[^a-z0-9.-]+/g, '-')\n .replace(/^[^a-z0-9]+/, '')\n .replace(/-+$/, '')\n .slice(0, MAX_NAME_LENGTH);\n\n const taken = new Set(existingNames);\n if (!taken.has(base)) {\n return base;\n }\n\n let suffix = 2;\n while (taken.has(`${base}-${suffix}`)) {\n suffix++;\n }\n return `${base}-${suffix}`;\n}\n\nexport function maskSecret(secret: string): string {\n if (secret.length <= 8) {\n return '•'.repeat(secret.length);\n }\n return `${secret.slice(0, 3)}${'•'.repeat(4)}${secret.slice(-2)}`;\n}\n\n/**\n * Stable signature for remembering an \"ignore\" choice across edits and reloads.\n * Hashing (FNV-1a) keeps the plaintext out of local storage, and deriving it\n * from content rather than position keeps it stable when the secret moves —\n * unlike the finding `id`.\n */\nexport function getFindingSignature(finding: Pick<SecretFinding, 'type' | 'secret'>): string {\n return `${finding.type}.${fnv1aHash(`${finding.type}:${finding.secret}`)}`;\n}\n\nfunction fnv1aHash(value: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < value.length; i++) {\n hash ^= value.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16);\n}\n"],"names":[],"mappings":"AAEA,MAAM,eAAA,GAAkB,GAAA;AAQjB,SAAS,iBAAA,CAAkB,OAAA,EAAsC,aAAA,GAA0B,EAAC,EAAW;AAC5G,EAAA,MAAM,IAAA,GAAO,GAAG,OAAA,CAAQ,IAAI,UACzB,WAAA,EAAY,CACZ,QAAQ,eAAA,EAAiB,GAAG,EAC5B,OAAA,CAAQ,aAAA,EAAe,EAAE,CAAA,CACzB,OAAA,CAAQ,OAAO,EAAE,CAAA,CACjB,KAAA,CAAM,CAAA,EAAG,eAAe,CAAA;AAE3B,EAAA,MAAM,KAAA,GAAQ,IAAI,GAAA,CAAI,aAAa,CAAA;AACnC,EAAA,IAAI,CAAC,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA,EAAG;AACpB,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,OAAO,MAAM,GAAA,CAAI,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,MAAM,EAAE,CAAA,EAAG;AACrC,IAAA,MAAA,EAAA;AAAA,EACF;AACA,EAAA,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,MAAM,CAAA,CAAA;AAC1B;AAEO,SAAS,WAAW,MAAA,EAAwB;AACjD,EAAA,IAAI,MAAA,CAAO,UAAU,CAAA,EAAG;AACtB,IAAA,OAAO,QAAA,CAAI,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA;AAAA,EACjC;AACA,EAAA,OAAO,CAAA,EAAG,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAC,CAAC,CAAA,EAAG,QAAA,CAAI,MAAA,CAAO,CAAC,CAAC,CAAA,EAAG,MAAA,CAAO,KAAA,CAAM,EAAE,CAAC,CAAA,CAAA;AACjE;AAQO,SAAS,oBAAoB,OAAA,EAAyD;AAC3F,EAAA,OAAO,CAAA,EAAG,OAAA,CAAQ,IAAI,CAAA,CAAA,EAAI,SAAA,CAAU,CAAA,EAAG,OAAA,CAAQ,IAAI,CAAA,CAAA,EAAI,OAAA,CAAQ,MAAM,CAAA,CAAE,CAAC,CAAA,CAAA;AAC1E;AAEA,SAAS,UAAU,KAAA,EAAuB;AACxC,EAAA,IAAI,IAAA,GAAO,UAAA;AACX,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,IAAA,IAAQ,KAAA,CAAM,WAAW,CAAC,CAAA;AAC1B,IAAA,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,QAAU,CAAA;AAAA,EACnC;AACA,EAAA,OAAA,CAAQ,IAAA,KAAS,CAAA,EAAG,QAAA,CAAS,EAAE,CAAA;AACjC;;;;"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createCodeScanner } from '../core/scanCode.js';
|
|
2
|
+
import '../core/detection/ruleDetectors.js';
|
|
3
|
+
import { DEFAULT_DEBOUNCE_MS } from '../core/constants.js';
|
|
4
|
+
|
|
5
|
+
const SECRET_MARKER_CODE = "hardcoded-secret";
|
|
6
|
+
const MARKER_OWNER = "secret-scanner";
|
|
7
|
+
const DEFAULT_LANGUAGES = ["typescript", "javascript"];
|
|
8
|
+
const DEFAULT_QUICK_FIX_TITLE = "Move to Secrets Manager";
|
|
9
|
+
function installSecretScanner(monaco, editor, options = {}) {
|
|
10
|
+
var _a, _b, _c, _d, _e;
|
|
11
|
+
const languages = (_a = options.languages) != null ? _a : DEFAULT_LANGUAGES;
|
|
12
|
+
const scan = (_b = options.scan) != null ? _b : createCodeScanner();
|
|
13
|
+
const debounceMs = (_c = options.debounceMs) != null ? _c : DEFAULT_DEBOUNCE_MS;
|
|
14
|
+
const markerMessage = (_d = options.markerMessage) != null ? _d : ((finding) => `Possible hardcoded secret (${finding.label}). Consider moving it to a managed secret.`);
|
|
15
|
+
const disposables = [];
|
|
16
|
+
const updateMarkers = () => {
|
|
17
|
+
const model = editor.getModel();
|
|
18
|
+
if (model === null) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (options.enabled === false) {
|
|
22
|
+
monaco.editor.setModelMarkers(model, MARKER_OWNER, []);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const script = model.getValue();
|
|
26
|
+
const markers = scan(script).map((finding) => {
|
|
27
|
+
const start = model.getPositionAt(finding.range.start);
|
|
28
|
+
const end = model.getPositionAt(finding.range.end);
|
|
29
|
+
return {
|
|
30
|
+
severity: monaco.MarkerSeverity.Warning,
|
|
31
|
+
message: markerMessage(finding),
|
|
32
|
+
code: SECRET_MARKER_CODE,
|
|
33
|
+
startLineNumber: start.lineNumber,
|
|
34
|
+
startColumn: start.column,
|
|
35
|
+
endLineNumber: end.lineNumber,
|
|
36
|
+
endColumn: end.column
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
monaco.editor.setModelMarkers(model, MARKER_OWNER, markers);
|
|
40
|
+
};
|
|
41
|
+
let debounceTimer;
|
|
42
|
+
const cancelScheduled = () => {
|
|
43
|
+
if (debounceTimer !== void 0) {
|
|
44
|
+
clearTimeout(debounceTimer);
|
|
45
|
+
debounceTimer = void 0;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const scheduleUpdate = () => {
|
|
49
|
+
cancelScheduled();
|
|
50
|
+
debounceTimer = setTimeout(() => {
|
|
51
|
+
debounceTimer = void 0;
|
|
52
|
+
updateMarkers();
|
|
53
|
+
}, debounceMs);
|
|
54
|
+
};
|
|
55
|
+
const updateNow = () => {
|
|
56
|
+
cancelScheduled();
|
|
57
|
+
updateMarkers();
|
|
58
|
+
};
|
|
59
|
+
updateMarkers();
|
|
60
|
+
disposables.push(editor.onDidChangeModelContent(() => scheduleUpdate()));
|
|
61
|
+
disposables.push(editor.onDidChangeModel(() => updateNow()));
|
|
62
|
+
disposables.push({ dispose: cancelScheduled });
|
|
63
|
+
if (options.enabled !== false && options.quickFix) {
|
|
64
|
+
const runQuickFix = options.quickFix.run;
|
|
65
|
+
const title = (_e = options.quickFix.title) != null ? _e : DEFAULT_QUICK_FIX_TITLE;
|
|
66
|
+
const commandId = editor.addCommand(0, (_accessor, finding) => runQuickFix(finding));
|
|
67
|
+
if (commandId) {
|
|
68
|
+
for (const language of languages) {
|
|
69
|
+
disposables.push(
|
|
70
|
+
monaco.languages.registerCodeActionProvider(language, {
|
|
71
|
+
provideCodeActions(model, _range, context) {
|
|
72
|
+
const secretMarkers = context.markers.filter((marker) => marker.code === SECRET_MARKER_CODE);
|
|
73
|
+
if (secretMarkers.length === 0) {
|
|
74
|
+
return { actions: [], dispose() {
|
|
75
|
+
} };
|
|
76
|
+
}
|
|
77
|
+
const findings = scan(model.getValue());
|
|
78
|
+
const actions = secretMarkers.flatMap((marker) => {
|
|
79
|
+
const markerOffset = model.getOffsetAt({
|
|
80
|
+
lineNumber: marker.startLineNumber,
|
|
81
|
+
column: marker.startColumn
|
|
82
|
+
});
|
|
83
|
+
const finding = findings.find((candidate) => candidate.range.start === markerOffset);
|
|
84
|
+
if (finding === void 0) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
title,
|
|
90
|
+
kind: "quickfix",
|
|
91
|
+
diagnostics: [marker],
|
|
92
|
+
command: { id: commandId, title, arguments: [finding] }
|
|
93
|
+
}
|
|
94
|
+
];
|
|
95
|
+
});
|
|
96
|
+
return { actions, dispose() {
|
|
97
|
+
} };
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
refresh: updateNow,
|
|
106
|
+
dispose() {
|
|
107
|
+
disposables.forEach((disposable) => disposable.dispose());
|
|
108
|
+
const model = editor.getModel();
|
|
109
|
+
if (model !== null) {
|
|
110
|
+
monaco.editor.setModelMarkers(model, MARKER_OWNER, []);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export { SECRET_MARKER_CODE, installSecretScanner };
|
|
117
|
+
//# sourceMappingURL=installSecretScanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installSecretScanner.js","sources":["../../../../src/secret-scanner/monaco/installSecretScanner.ts"],"sourcesContent":["import { createCodeScanner, DEFAULT_DEBOUNCE_MS, type SecretFinding } from '../core/index';\nimport type { IStandaloneCodeEditor, MonacoNamespace } from './types';\n\n/** Marker `code` identifying the scanner's diagnostics for the quick fix. */\nexport const SECRET_MARKER_CODE = 'hardcoded-secret';\n\nconst MARKER_OWNER = 'secret-scanner';\nconst DEFAULT_LANGUAGES = ['typescript', 'javascript'];\nconst DEFAULT_QUICK_FIX_TITLE = 'Move to Secrets Manager';\n\nexport interface InstallSecretScannerOptions {\n languages?: string[];\n enabled?: boolean;\n markerMessage?: (finding: SecretFinding) => string;\n quickFix?: { title?: string; run: (finding: SecretFinding) => void };\n scan?: (code: string) => SecretFinding[];\n debounceMs?: number;\n}\n\nexport interface SecretScannerInstallation {\n refresh(): void;\n dispose(): void;\n}\n\n/**\n * Gutter markers for detected secrets plus an optional \"Move to Secrets Manager\"\n * quick fix.\n */\nexport function installSecretScanner(\n monaco: MonacoNamespace,\n editor: IStandaloneCodeEditor,\n options: InstallSecretScannerOptions = {}\n): SecretScannerInstallation {\n const languages = options.languages ?? DEFAULT_LANGUAGES;\n const scan = options.scan ?? createCodeScanner();\n const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;\n const markerMessage =\n options.markerMessage ??\n ((finding: SecretFinding) =>\n `Possible hardcoded secret (${finding.label}). Consider moving it to a managed secret.`);\n\n const disposables: Array<{ dispose(): void }> = [];\n\n const updateMarkers = () => {\n const model = editor.getModel();\n if (model === null) {\n return;\n }\n\n if (options.enabled === false) {\n monaco.editor.setModelMarkers(model, MARKER_OWNER, []);\n return;\n }\n\n const script = model.getValue();\n const markers = scan(script).map((finding) => {\n const start = model.getPositionAt(finding.range.start);\n const end = model.getPositionAt(finding.range.end);\n return {\n severity: monaco.MarkerSeverity.Warning,\n message: markerMessage(finding),\n code: SECRET_MARKER_CODE,\n startLineNumber: start.lineNumber,\n startColumn: start.column,\n endLineNumber: end.lineNumber,\n endColumn: end.column,\n };\n });\n\n monaco.editor.setModelMarkers(model, MARKER_OWNER, markers);\n };\n\n let debounceTimer: ReturnType<typeof setTimeout> | undefined;\n const cancelScheduled = () => {\n if (debounceTimer !== undefined) {\n clearTimeout(debounceTimer);\n debounceTimer = undefined;\n }\n };\n const scheduleUpdate = () => {\n cancelScheduled();\n debounceTimer = setTimeout(() => {\n debounceTimer = undefined;\n updateMarkers();\n }, debounceMs);\n };\n const updateNow = () => {\n cancelScheduled();\n updateMarkers();\n };\n\n updateMarkers();\n disposables.push(editor.onDidChangeModelContent(() => scheduleUpdate()));\n disposables.push(editor.onDidChangeModel(() => updateNow()));\n disposables.push({ dispose: cancelScheduled });\n\n if (options.enabled !== false && options.quickFix) {\n const runQuickFix = options.quickFix.run;\n const title = options.quickFix.title ?? DEFAULT_QUICK_FIX_TITLE;\n\n const commandId = editor.addCommand(0, (_accessor: unknown, finding: SecretFinding) => runQuickFix(finding));\n\n if (commandId) {\n for (const language of languages) {\n disposables.push(\n monaco.languages.registerCodeActionProvider(language, {\n provideCodeActions(model, _range, context) {\n const secretMarkers = context.markers.filter((marker) => marker.code === SECRET_MARKER_CODE);\n\n if (secretMarkers.length === 0) {\n return { actions: [], dispose() {} };\n }\n\n const findings = scan(model.getValue());\n const actions = secretMarkers.flatMap((marker) => {\n const markerOffset = model.getOffsetAt({\n lineNumber: marker.startLineNumber,\n column: marker.startColumn,\n });\n const finding = findings.find((candidate) => candidate.range.start === markerOffset);\n\n if (finding === undefined) {\n return [];\n }\n\n return [\n {\n title,\n kind: 'quickfix',\n diagnostics: [marker],\n command: { id: commandId, title, arguments: [finding] },\n },\n ];\n });\n\n return { actions, dispose() {} };\n },\n })\n );\n }\n }\n }\n\n return {\n refresh: updateNow,\n dispose() {\n disposables.forEach((disposable) => disposable.dispose());\n const model = editor.getModel();\n if (model !== null) {\n monaco.editor.setModelMarkers(model, MARKER_OWNER, []);\n }\n },\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, Modal, Stack, Input, ClipboardButton } from '@grafana/ui';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TITLE = "Reference your secret";
|
|
5
|
+
function SecretReferenceModal({
|
|
6
|
+
reference,
|
|
7
|
+
onDismiss,
|
|
8
|
+
snippet,
|
|
9
|
+
title = DEFAULT_TITLE,
|
|
10
|
+
description
|
|
11
|
+
}) {
|
|
12
|
+
if (reference === null) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const text = snippet != null ? snippet : reference.reference;
|
|
16
|
+
const defaultDescription = /* @__PURE__ */ React.createElement(Text, { variant: "body" }, "The secret ", /* @__PURE__ */ React.createElement("code", null, reference.secretName), " was created, but this value could not be replaced automatically. Copy the reference below and use it in place of the hardcoded value.");
|
|
17
|
+
return /* @__PURE__ */ React.createElement(Modal, { title, isOpen: true, onDismiss }, /* @__PURE__ */ React.createElement(Stack, { direction: "column", gap: 2 }, description != null ? description : defaultDescription, /* @__PURE__ */ React.createElement(Input, { readOnly: true, value: text })), /* @__PURE__ */ React.createElement(Modal.ButtonRow, null, /* @__PURE__ */ React.createElement(ClipboardButton, { icon: "copy", variant: "primary", getText: () => text }, "Copy reference")));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { SecretReferenceModal };
|
|
21
|
+
//# sourceMappingURL=SecretReferenceModal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SecretReferenceModal.js","sources":["../../../../src/secret-scanner/panel/SecretReferenceModal.tsx"],"sourcesContent":["import React from 'react';\nimport { ClipboardButton, Input, Modal, Stack, Text } from '@grafana/ui';\n\nimport type { SecretReference } from '../react/index';\n\ninterface SecretReferenceModalProps {\n reference: SecretReference | null;\n onDismiss: () => void;\n snippet?: string;\n title?: string;\n description?: NonNullable<React.ReactNode>;\n}\n\nconst DEFAULT_TITLE = 'Reference your secret';\n\n/**\n * Shown after a secret is created for a finding that couldn't be rewritten in\n * place. Purely presentational — render it off `scanner.reference.pending`.\n */\nexport function SecretReferenceModal({\n reference,\n onDismiss,\n snippet,\n title = DEFAULT_TITLE,\n description,\n}: SecretReferenceModalProps) {\n if (reference === null) {\n return null;\n }\n\n const text = snippet ?? reference.reference;\n const defaultDescription = (\n <Text variant=\"body\">\n The secret <code>{reference.secretName}</code> was created, but this value could not be replaced automatically.\n Copy the reference below and use it in place of the hardcoded value.\n </Text>\n );\n\n return (\n <Modal title={title} isOpen onDismiss={onDismiss}>\n <Stack direction=\"column\" gap={2}>\n {description ?? defaultDescription}\n\n <Input readOnly value={text} />\n </Stack>\n\n <Modal.ButtonRow>\n <ClipboardButton icon=\"copy\" variant=\"primary\" getText={() => text}>\n Copy reference\n </ClipboardButton>\n </Modal.ButtonRow>\n </Modal>\n );\n}\n"],"names":[],"mappings":";;;AAaA,MAAM,aAAA,GAAgB,uBAAA;AAMf,SAAS,oBAAA,CAAqB;AAAA,EACnC,SAAA;AAAA,EACA,SAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA,GAAQ,aAAA;AAAA,EACR;AACF,CAAA,EAA8B;AAC5B,EAAA,IAAI,cAAc,IAAA,EAAM;AACtB,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,IAAA,GAAO,4BAAW,SAAA,CAAU,SAAA;AAClC,EAAA,MAAM,kBAAA,mBACJ,KAAA,CAAA,aAAA,CAAC,IAAA,EAAA,EAAK,OAAA,EAAQ,MAAA,EAAA,EAAO,aAAA,kBACR,KAAA,CAAA,aAAA,CAAC,MAAA,EAAA,IAAA,EAAM,SAAA,CAAU,UAAW,CAAA,EAAO,wIAEhD,CAAA;AAGF,EAAA,2CACG,KAAA,EAAA,EAAM,KAAA,EAAc,MAAA,EAAM,IAAA,EAAC,6BAC1B,KAAA,CAAA,aAAA,CAAC,KAAA,EAAA,EAAM,SAAA,EAAU,QAAA,EAAS,KAAK,CAAA,EAAA,EAC5B,WAAA,IAAA,IAAA,GAAA,WAAA,GAAe,kBAAA,kBAEhB,KAAA,CAAA,aAAA,CAAC,SAAM,QAAA,EAAQ,IAAA,EAAC,KAAA,EAAO,IAAA,EAAM,CAC/B,CAAA,kBAEA,KAAA,CAAA,aAAA,CAAC,KAAA,CAAM,SAAA,EAAN,sBACC,KAAA,CAAA,aAAA,CAAC,eAAA,EAAA,EAAgB,IAAA,EAAK,MAAA,EAAO,SAAQ,SAAA,EAAU,OAAA,EAAS,MAAM,IAAA,EAAA,EAAM,gBAEpE,CACF,CACF,CAAA;AAEJ;;;;"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useStyles2, Alert, Text, Stack, Badge, Button } from '@grafana/ui';
|
|
3
|
+
import { css } from '@emotion/css';
|
|
4
|
+
import '../core/detection/literals.js';
|
|
5
|
+
import '../core/detection/ruleDetectors.js';
|
|
6
|
+
import { maskSecret } from '../core/secretScanner.utils.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_DESCRIPTION = "Hardcoded secrets in test scripts are stored in plaintext. Move them to Secrets Manager and reference them at runtime instead.";
|
|
9
|
+
const DEFAULT_MOVE_LABEL = "Move to secret";
|
|
10
|
+
const DEFAULT_IGNORE_LABEL = "Ignore";
|
|
11
|
+
const CONFIDENCE_BADGE = {
|
|
12
|
+
high: { text: "high", color: "red" },
|
|
13
|
+
medium: { text: "medium", color: "orange" },
|
|
14
|
+
low: { text: "low", color: "blue" }
|
|
15
|
+
};
|
|
16
|
+
function SecretFindingsPanel({
|
|
17
|
+
findings,
|
|
18
|
+
onMove,
|
|
19
|
+
onIgnore,
|
|
20
|
+
hidden = false,
|
|
21
|
+
showLine = true,
|
|
22
|
+
description = DEFAULT_DESCRIPTION,
|
|
23
|
+
moveLabel = DEFAULT_MOVE_LABEL,
|
|
24
|
+
ignoreLabel = DEFAULT_IGNORE_LABEL
|
|
25
|
+
}) {
|
|
26
|
+
const styles = useStyles2(getStyles);
|
|
27
|
+
if (hidden || findings.length === 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const title = findings.length === 1 ? "1 potential secret detected" : `${findings.length} potential secrets detected`;
|
|
31
|
+
return /* @__PURE__ */ React.createElement(Alert, { severity: "warning", title }, /* @__PURE__ */ React.createElement(Text, { variant: "bodySmall" }, description), /* @__PURE__ */ React.createElement("div", { className: styles.list }, findings.map((finding) => /* @__PURE__ */ React.createElement("div", { key: finding.id, className: styles.row }, /* @__PURE__ */ React.createElement(Stack, { alignItems: "center", gap: 1, wrap: "wrap" }, /* @__PURE__ */ React.createElement(Badge, { text: finding.label, color: CONFIDENCE_BADGE[finding.confidence].color }), /* @__PURE__ */ React.createElement("code", { className: styles.value }, maskSecret(finding.secret)), /* @__PURE__ */ React.createElement(Text, { variant: "bodySmall", color: "secondary" }, showLine && finding.line != null ? `line ${finding.line} \xB7 ` : "", CONFIDENCE_BADGE[finding.confidence].text, " confidence")), /* @__PURE__ */ React.createElement(Stack, { gap: 1 }, onMove && /* @__PURE__ */ React.createElement(Button, { size: "sm", variant: "primary", icon: "shield", onClick: () => onMove(finding) }, moveLabel), onIgnore && /* @__PURE__ */ React.createElement(Button, { size: "sm", variant: "secondary", fill: "outline", onClick: () => onIgnore(finding) }, ignoreLabel))))));
|
|
32
|
+
}
|
|
33
|
+
function SecretScannerPanel({ scanner, readOnly = false, ...rest }) {
|
|
34
|
+
return /* @__PURE__ */ React.createElement(
|
|
35
|
+
SecretFindingsPanel,
|
|
36
|
+
{
|
|
37
|
+
findings: scanner.findings,
|
|
38
|
+
onMove: readOnly ? void 0 : scanner.migration.request,
|
|
39
|
+
onIgnore: scanner.ignore,
|
|
40
|
+
...rest
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
function getStyles(theme) {
|
|
45
|
+
return {
|
|
46
|
+
list: css`
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
gap: ${theme.spacing(1)};
|
|
50
|
+
margin-top: ${theme.spacing(1)};
|
|
51
|
+
/* Cap the list so a script with many findings can't grow the panel
|
|
52
|
+
unbounded and push the editor out of view; scroll past ~8 rows. */
|
|
53
|
+
max-height: ${theme.spacing(40)};
|
|
54
|
+
overflow-y: auto;
|
|
55
|
+
`,
|
|
56
|
+
row: css`
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: space-between;
|
|
60
|
+
gap: ${theme.spacing(2)};
|
|
61
|
+
padding: ${theme.spacing(1)};
|
|
62
|
+
border: 1px solid ${theme.colors.border.weak};
|
|
63
|
+
border-radius: ${theme.shape.radius.default};
|
|
64
|
+
background: ${theme.colors.background.secondary};
|
|
65
|
+
flex-wrap: wrap;
|
|
66
|
+
`,
|
|
67
|
+
value: css`
|
|
68
|
+
font-family: ${theme.typography.fontFamilyMonospace};
|
|
69
|
+
font-size: ${theme.typography.bodySmall.fontSize};
|
|
70
|
+
`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { SecretFindingsPanel, SecretScannerPanel };
|
|
75
|
+
//# sourceMappingURL=SecretScannerPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SecretScannerPanel.js","sources":["../../../../src/secret-scanner/panel/SecretScannerPanel.tsx"],"sourcesContent":["import React from 'react';\nimport { Alert, Badge, Button, Stack, Text, useStyles2 } from '@grafana/ui';\nimport type { GrafanaTheme2 } from '@grafana/data';\nimport { css } from '@emotion/css';\n\nimport { maskSecret, type SecretConfidence, type SecretFinding } from '../core/index';\nimport type { SecretScanner } from '../react/index';\n\nexport interface SecretFindingSummary {\n id: string;\n label: string;\n confidence: SecretConfidence;\n secret: string;\n line?: number;\n}\n\ninterface SecretFindingsPanelProps<T extends SecretFindingSummary> {\n findings: T[];\n onMove?: (finding: T) => void;\n onIgnore?: (finding: T) => void;\n hidden?: boolean;\n showLine?: boolean;\n description?: NonNullable<React.ReactNode>;\n moveLabel?: React.ReactNode;\n ignoreLabel?: React.ReactNode;\n}\n\ninterface SecretScannerPanelProps extends Omit<\n SecretFindingsPanelProps<SecretFinding>,\n 'findings' | 'onMove' | 'onIgnore'\n> {\n scanner: SecretScanner;\n readOnly?: boolean;\n}\n\nconst DEFAULT_DESCRIPTION =\n 'Hardcoded secrets in test scripts are stored in plaintext. Move them to Secrets Manager and reference them at runtime instead.';\nconst DEFAULT_MOVE_LABEL = 'Move to secret';\nconst DEFAULT_IGNORE_LABEL = 'Ignore';\n\nconst CONFIDENCE_BADGE: Record<SecretConfidence, { text: string; color: 'red' | 'orange' | 'blue' }> = {\n high: { text: 'high', color: 'red' },\n medium: { text: 'medium', color: 'orange' },\n low: { text: 'low', color: 'blue' },\n};\n\n/**\n * Presentational: takes findings and renders them. Accepts any finding shape\n * carrying the fields in `SecretFindingSummary`, so a host can pass its own.\n * For a panel driven by `useSecretScanner`, use `SecretScannerPanel`.\n */\nexport function SecretFindingsPanel<T extends SecretFindingSummary>({\n findings,\n onMove,\n onIgnore,\n hidden = false,\n showLine = true,\n description = DEFAULT_DESCRIPTION,\n moveLabel = DEFAULT_MOVE_LABEL,\n ignoreLabel = DEFAULT_IGNORE_LABEL,\n}: SecretFindingsPanelProps<T>) {\n const styles = useStyles2(getStyles);\n\n if (hidden || findings.length === 0) {\n return null;\n }\n\n const title = findings.length === 1 ? '1 potential secret detected' : `${findings.length} potential secrets detected`;\n\n return (\n <Alert severity=\"warning\" title={title}>\n <Text variant=\"bodySmall\">{description}</Text>\n\n <div className={styles.list}>\n {findings.map((finding) => (\n <div key={finding.id} className={styles.row}>\n <Stack alignItems=\"center\" gap={1} wrap=\"wrap\">\n <Badge text={finding.label} color={CONFIDENCE_BADGE[finding.confidence].color} />\n <code className={styles.value}>{maskSecret(finding.secret)}</code>\n <Text variant=\"bodySmall\" color=\"secondary\">\n {showLine && finding.line != null ? `line ${finding.line} · ` : ''}\n {CONFIDENCE_BADGE[finding.confidence].text} confidence\n </Text>\n </Stack>\n\n <Stack gap={1}>\n {onMove && (\n <Button size=\"sm\" variant=\"primary\" icon=\"shield\" onClick={() => onMove(finding)}>\n {moveLabel}\n </Button>\n )}\n {onIgnore && (\n <Button size=\"sm\" variant=\"secondary\" fill=\"outline\" onClick={() => onIgnore(finding)}>\n {ignoreLabel}\n </Button>\n )}\n </Stack>\n </div>\n ))}\n </div>\n </Alert>\n );\n}\n\n/**\n * \"Move to secret\" calls `scanner.migration.request(finding)`, which sets\n * `scanner.migration.finding`; render your own create-secret modal off that, and\n * call `scanner.migration.apply` once the secret exists.\n */\nexport function SecretScannerPanel({ scanner, readOnly = false, ...rest }: SecretScannerPanelProps) {\n return (\n <SecretFindingsPanel\n findings={scanner.findings}\n onMove={readOnly ? undefined : scanner.migration.request}\n onIgnore={scanner.ignore}\n {...rest}\n />\n );\n}\n\nfunction getStyles(theme: GrafanaTheme2) {\n return {\n list: css`\n display: flex;\n flex-direction: column;\n gap: ${theme.spacing(1)};\n margin-top: ${theme.spacing(1)};\n /* Cap the list so a script with many findings can't grow the panel\n unbounded and push the editor out of view; scroll past ~8 rows. */\n max-height: ${theme.spacing(40)};\n overflow-y: auto;\n `,\n row: css`\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: ${theme.spacing(2)};\n padding: ${theme.spacing(1)};\n border: 1px solid ${theme.colors.border.weak};\n border-radius: ${theme.shape.radius.default};\n background: ${theme.colors.background.secondary};\n flex-wrap: wrap;\n `,\n value: css`\n font-family: ${theme.typography.fontFamilyMonospace};\n font-size: ${theme.typography.bodySmall.fontSize};\n `,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import '../core/detection/literals.js';
|
|
3
|
+
import '../core/detection/ruleDetectors.js';
|
|
4
|
+
import { getFindingSignature } from '../core/secretScanner.utils.js';
|
|
5
|
+
import { useLocalStorageList } from './useLocalStorageList.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_STORAGE_KEY = "secretScanner.ignored";
|
|
8
|
+
function useIgnoredSecrets(storageKey = DEFAULT_STORAGE_KEY) {
|
|
9
|
+
const [ignored, setIgnored] = useLocalStorageList(storageKey);
|
|
10
|
+
const isIgnored = useCallback(
|
|
11
|
+
(finding) => ignored.includes(getFindingSignature(finding)),
|
|
12
|
+
[ignored]
|
|
13
|
+
);
|
|
14
|
+
const ignore = useCallback(
|
|
15
|
+
(finding) => {
|
|
16
|
+
const signature = getFindingSignature(finding);
|
|
17
|
+
setIgnored((current) => current.includes(signature) ? current : [...current, signature]);
|
|
18
|
+
},
|
|
19
|
+
[setIgnored]
|
|
20
|
+
);
|
|
21
|
+
return { isIgnored, ignore };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { useIgnoredSecrets };
|
|
25
|
+
//# sourceMappingURL=useIgnoredSecrets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useIgnoredSecrets.js","sources":["../../../../src/secret-scanner/react/useIgnoredSecrets.ts"],"sourcesContent":["import { useCallback } from 'react';\n\nimport { getFindingSignature, type SecretFinding } from '../core/index';\nimport { useLocalStorageList } from './useLocalStorageList';\n\nconst DEFAULT_STORAGE_KEY = 'secretScanner.ignored';\n\ntype IgnorableFinding = Pick<SecretFinding, 'type' | 'secret'>;\n\n/**\n * Remembers which detected secrets the user chose to ignore, persisted in\n * localStorage under `storageKey` (namespace it per document). Tracked by a\n * content-derived signature, never the plaintext.\n */\nexport function useIgnoredSecrets(storageKey: string = DEFAULT_STORAGE_KEY) {\n const [ignored, setIgnored] = useLocalStorageList(storageKey);\n\n const isIgnored = useCallback(\n (finding: IgnorableFinding) => ignored.includes(getFindingSignature(finding)),\n [ignored]\n );\n\n const ignore = useCallback(\n (finding: IgnorableFinding) => {\n const signature = getFindingSignature(finding);\n setIgnored((current) => (current.includes(signature) ? current : [...current, signature]));\n },\n [setIgnored]\n );\n\n return { isIgnored, ignore };\n}\n"],"names":[],"mappings":";;;;;;AAKA,MAAM,mBAAA,GAAsB,uBAAA;AASrB,SAAS,iBAAA,CAAkB,aAAqB,mBAAA,EAAqB;AAC1E,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,oBAAoB,UAAU,CAAA;AAE5D,EAAA,MAAM,SAAA,GAAY,WAAA;AAAA,IAChB,CAAC,OAAA,KAA8B,OAAA,CAAQ,QAAA,CAAS,mBAAA,CAAoB,OAAO,CAAC,CAAA;AAAA,IAC5E,CAAC,OAAO;AAAA,GACV;AAEA,EAAA,MAAM,MAAA,GAAS,WAAA;AAAA,IACb,CAAC,OAAA,KAA8B;AAC7B,MAAA,MAAM,SAAA,GAAY,oBAAoB,OAAO,CAAA;AAC7C,MAAA,UAAA,CAAW,CAAC,OAAA,KAAa,OAAA,CAAQ,QAAA,CAAS,SAAS,CAAA,GAAI,OAAA,GAAU,CAAC,GAAG,OAAA,EAAS,SAAS,CAAE,CAAA;AAAA,IAC3F,CAAA;AAAA,IACA,CAAC,UAAU;AAAA,GACb;AAEA,EAAA,OAAO,EAAE,WAAW,MAAA,EAAO;AAC7B;;;;"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
function read(key) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = localStorage.getItem(key);
|
|
6
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
7
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
8
|
+
} catch (e) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function useLocalStorageList(key) {
|
|
13
|
+
const [value, setValue] = useState(() => read(key));
|
|
14
|
+
const keyRef = useRef(key);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (keyRef.current !== key) {
|
|
17
|
+
keyRef.current = key;
|
|
18
|
+
setValue(read(key));
|
|
19
|
+
}
|
|
20
|
+
}, [key]);
|
|
21
|
+
const set = useCallback(
|
|
22
|
+
(updater) => {
|
|
23
|
+
setValue((prev) => {
|
|
24
|
+
const next = typeof updater === "function" ? updater(prev) : updater;
|
|
25
|
+
try {
|
|
26
|
+
localStorage.setItem(key, JSON.stringify(next));
|
|
27
|
+
} catch (e) {
|
|
28
|
+
}
|
|
29
|
+
return next;
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
[key]
|
|
33
|
+
);
|
|
34
|
+
return [value, set];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { useLocalStorageList };
|
|
38
|
+
//# sourceMappingURL=useLocalStorageList.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLocalStorageList.js","sources":["../../../../src/secret-scanner/react/useLocalStorageList.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nfunction read(key: string): string[] {\n try {\n const raw = localStorage.getItem(key);\n const parsed = raw ? JSON.parse(raw) : [];\n return Array.isArray(parsed) ? parsed : [];\n } catch {\n return [];\n }\n}\n\ntype Updater = string[] | ((prev: string[]) => string[]);\n\nexport function useLocalStorageList(key: string): [string[], (updater: Updater) => void] {\n const [value, setValue] = useState<string[]>(() => read(key));\n const keyRef = useRef(key);\n\n useEffect(() => {\n if (keyRef.current !== key) {\n keyRef.current = key;\n setValue(read(key));\n }\n }, [key]);\n\n const set = useCallback(\n (updater: Updater) => {\n setValue((prev) => {\n const next = typeof updater === 'function' ? updater(prev) : updater;\n try {\n localStorage.setItem(key, JSON.stringify(next));\n } catch {\n // Ignore write failures (private mode, quota, …).\n }\n return next;\n });\n },\n [key]\n );\n\n return [value, set];\n}\n"],"names":[],"mappings":";;AAEA,SAAS,KAAK,GAAA,EAAuB;AACnC,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,YAAA,CAAa,OAAA,CAAQ,GAAG,CAAA;AACpC,IAAA,MAAM,SAAS,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,GAAG,IAAI,EAAC;AACxC,IAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GAAI,SAAS,EAAC;AAAA,EAC3C,CAAA,CAAA,OAAQ,CAAA,EAAA;AACN,IAAA,OAAO,EAAC;AAAA,EACV;AACF;AAIO,SAAS,oBAAoB,GAAA,EAAqD;AACvF,EAAA,MAAM,CAAC,OAAO,QAAQ,CAAA,GAAI,SAAmB,MAAM,IAAA,CAAK,GAAG,CAAC,CAAA;AAC5D,EAAA,MAAM,MAAA,GAAS,OAAO,GAAG,CAAA;AAEzB,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,MAAA,CAAO,YAAY,GAAA,EAAK;AAC1B,MAAA,MAAA,CAAO,OAAA,GAAU,GAAA;AACjB,MAAA,QAAA,CAAS,IAAA,CAAK,GAAG,CAAC,CAAA;AAAA,IACpB;AAAA,EACF,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AAER,EAAA,MAAM,GAAA,GAAM,WAAA;AAAA,IACV,CAAC,OAAA,KAAqB;AACpB,MAAA,QAAA,CAAS,CAAC,IAAA,KAAS;AACjB,QAAA,MAAM,OAAO,OAAO,OAAA,KAAY,UAAA,GAAa,OAAA,CAAQ,IAAI,CAAA,GAAI,OAAA;AAC7D,QAAA,IAAI;AACF,UAAA,YAAA,CAAa,OAAA,CAAQ,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,IAAI,CAAC,CAAA;AAAA,QAChD,CAAA,CAAA,OAAQ,CAAA,EAAA;AAAA,QAER;AACA,QAAA,OAAO,IAAA;AAAA,MACT,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IACA,CAAC,GAAG;AAAA,GACN;AAEA,EAAA,OAAO,CAAC,OAAO,GAAG,CAAA;AACpB;;;;"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { installSecretScanner } from '../monaco/installSecretScanner.js';
|
|
3
|
+
import { useSecretScanner } from './useSecretScanner.js';
|
|
4
|
+
|
|
5
|
+
function useMonacoSecretScanner(params) {
|
|
6
|
+
const { monaco, editor, canMigrate = true, languages, ...headless } = params;
|
|
7
|
+
const { enabled = true, debounceMs, scan } = headless;
|
|
8
|
+
const scanner = useSecretScanner(headless);
|
|
9
|
+
const { request } = scanner.migration;
|
|
10
|
+
const languageKey = (languages != null ? languages : []).join(",");
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (monaco === void 0 || editor === void 0) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const installation = installSecretScanner(monaco, editor, {
|
|
16
|
+
enabled,
|
|
17
|
+
languages,
|
|
18
|
+
debounceMs,
|
|
19
|
+
scan,
|
|
20
|
+
quickFix: canMigrate ? { run: request } : void 0
|
|
21
|
+
});
|
|
22
|
+
return () => installation.dispose();
|
|
23
|
+
}, [monaco, editor, enabled, canMigrate, languageKey, debounceMs, scan, request]);
|
|
24
|
+
return scanner;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { useMonacoSecretScanner };
|
|
28
|
+
//# sourceMappingURL=useMonacoSecretScanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMonacoSecretScanner.js","sources":["../../../../src/secret-scanner/react/useMonacoSecretScanner.ts"],"sourcesContent":["import { useEffect } from 'react';\n\nimport { installSecretScanner, type IStandaloneCodeEditor, type MonacoNamespace } from '../monaco/index';\nimport { useSecretScanner, type SecretScanner, type UseSecretScannerParams } from './useSecretScanner';\n\nexport interface UseMonacoSecretScannerParams extends UseSecretScannerParams {\n /** Monaco namespace + editor, injected from the host's editor mount. */\n monaco?: MonacoNamespace;\n editor?: IStandaloneCodeEditor;\n /** Whether the \"Move to secret\" quick fix is offered. Default true. */\n canMigrate?: boolean;\n /** Languages to register the quick fix for. Default: ts + js. */\n languages?: string[];\n}\n\n/**\n * Monaco adapter over {@link useSecretScanner}: adds gutter markers and a\n * \"Move to Secrets Manager\" quick fix on top of the headless scanner.\n *\n * The Monaco pieces are optional — until `monaco` and `editor` are both\n * provided, this behaves exactly like `useSecretScanner`, so a host can mount it\n * before its editor is ready.\n */\nexport function useMonacoSecretScanner(params: UseMonacoSecretScannerParams): SecretScanner {\n const { monaco, editor, canMigrate = true, languages, ...headless } = params;\n const { enabled = true, debounceMs, scan } = headless;\n\n const scanner = useSecretScanner(headless);\n const { request } = scanner.migration;\n\n const languageKey = (languages ?? []).join(',');\n useEffect(() => {\n if (monaco === undefined || editor === undefined) {\n return;\n }\n\n const installation = installSecretScanner(monaco, editor, {\n enabled,\n languages,\n debounceMs,\n scan,\n quickFix: canMigrate ? { run: request } : undefined,\n });\n\n return () => installation.dispose();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [monaco, editor, enabled, canMigrate, languageKey, debounceMs, scan, request]);\n\n return scanner;\n}\n"],"names":[],"mappings":";;;;AAuBO,SAAS,uBAAuB,MAAA,EAAqD;AAC1F,EAAA,MAAM,EAAE,QAAQ,MAAA,EAAQ,UAAA,GAAa,MAAM,SAAA,EAAW,GAAG,UAAS,GAAI,MAAA;AACtE,EAAA,MAAM,EAAE,OAAA,GAAU,IAAA,EAAM,UAAA,EAAY,MAAK,GAAI,QAAA;AAE7C,EAAA,MAAM,OAAA,GAAU,iBAAiB,QAAQ,CAAA;AACzC,EAAA,MAAM,EAAE,OAAA,EAAQ,GAAI,OAAA,CAAQ,SAAA;AAE5B,EAAA,MAAM,WAAA,GAAA,CAAe,SAAA,IAAA,IAAA,GAAA,SAAA,GAAa,EAAC,EAAG,KAAK,GAAG,CAAA;AAC9C,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,MAAA,KAAW,MAAA,IAAa,MAAA,KAAW,MAAA,EAAW;AAChD,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,YAAA,GAAe,oBAAA,CAAqB,MAAA,EAAQ,MAAA,EAAQ;AAAA,MACxD,OAAA;AAAA,MACA,SAAA;AAAA,MACA,UAAA;AAAA,MACA,IAAA;AAAA,MACA,QAAA,EAAU,UAAA,GAAa,EAAE,GAAA,EAAK,SAAQ,GAAI;AAAA,KAC3C,CAAA;AAED,IAAA,OAAO,MAAM,aAAa,OAAA,EAAQ;AAAA,EAEpC,CAAA,EAAG,CAAC,MAAA,EAAQ,MAAA,EAAQ,OAAA,EAAS,YAAY,WAAA,EAAa,UAAA,EAAY,IAAA,EAAM,OAAO,CAAC,CAAA;AAEhF,EAAA,OAAO,OAAA;AACT;;;;"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useMemo, useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { createCodeScanner } from '../core/scanCode.js';
|
|
3
|
+
import '../core/detection/ruleDetectors.js';
|
|
4
|
+
import { DEFAULT_DEBOUNCE_MS } from '../core/constants.js';
|
|
5
|
+
import { k6SecretRewriter } from '../core/k6Rewriter.js';
|
|
6
|
+
import { useIgnoredSecrets } from './useIgnoredSecrets.js';
|
|
7
|
+
|
|
8
|
+
function useSecretScanner(params) {
|
|
9
|
+
const {
|
|
10
|
+
text,
|
|
11
|
+
scan,
|
|
12
|
+
rewriter = k6SecretRewriter,
|
|
13
|
+
onChange,
|
|
14
|
+
enabled = true,
|
|
15
|
+
ignoreStorageKey,
|
|
16
|
+
debounceMs = DEFAULT_DEBOUNCE_MS
|
|
17
|
+
} = params;
|
|
18
|
+
const defaultScan = useMemo(() => createCodeScanner(), []);
|
|
19
|
+
const scanner = scan != null ? scan : defaultScan;
|
|
20
|
+
const { isIgnored, ignore } = useIgnoredSecrets(ignoreStorageKey);
|
|
21
|
+
const [activeFinding, setActiveFinding] = useState(null);
|
|
22
|
+
const [pendingReference, setPendingReference] = useState(null);
|
|
23
|
+
const [findings, setFindings] = useState(() => enabled ? scanner(text) : []);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!enabled) {
|
|
26
|
+
setFindings([]);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const handle = setTimeout(() => setFindings(scanner(text)), debounceMs);
|
|
30
|
+
return () => clearTimeout(handle);
|
|
31
|
+
}, [text, debounceMs, enabled, scanner]);
|
|
32
|
+
const visible = useMemo(() => findings.filter((finding) => !isIgnored(finding)), [findings, isIgnored]);
|
|
33
|
+
const request = useCallback((finding) => setActiveFinding(finding), []);
|
|
34
|
+
const cancel = useCallback(() => setActiveFinding(null), []);
|
|
35
|
+
const dismiss = useCallback(() => setPendingReference(null), []);
|
|
36
|
+
const apply = useCallback(
|
|
37
|
+
(finding, secretName) => {
|
|
38
|
+
const result = onChange ? rewriter.apply(text, finding, secretName) : void 0;
|
|
39
|
+
if (result == null ? void 0 : result.applied) {
|
|
40
|
+
onChange(result.text);
|
|
41
|
+
} else {
|
|
42
|
+
setPendingReference({ finding, secretName, reference: rewriter.reference(secretName) });
|
|
43
|
+
}
|
|
44
|
+
setActiveFinding(null);
|
|
45
|
+
},
|
|
46
|
+
[text, onChange, rewriter]
|
|
47
|
+
);
|
|
48
|
+
const migration = useMemo(
|
|
49
|
+
() => ({ finding: activeFinding, request, cancel, apply }),
|
|
50
|
+
[activeFinding, request, cancel, apply]
|
|
51
|
+
);
|
|
52
|
+
const reference = useMemo(() => ({ pending: pendingReference, dismiss }), [pendingReference, dismiss]);
|
|
53
|
+
return { findings: visible, ignore, migration, reference };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { useSecretScanner };
|
|
57
|
+
//# sourceMappingURL=useSecretScanner.js.map
|