fixyoursecret 0.3.1-developer-preview.1
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 +45 -0
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/bin/index.js +116 -0
- package/commands/fix.js +88 -0
- package/commands/hook.js +37 -0
- package/commands/init.js +30 -0
- package/commands/rotate.js +146 -0
- package/commands/scan.js +184 -0
- package/detectors/anthropic.js +16 -0
- package/detectors/aws.js +16 -0
- package/detectors/cohere.js +16 -0
- package/detectors/generic.js +55 -0
- package/detectors/github.js +16 -0
- package/detectors/gitlab.js +16 -0
- package/detectors/google.js +16 -0
- package/detectors/huggingface.js +16 -0
- package/detectors/mailgun.js +16 -0
- package/detectors/npmToken.js +16 -0
- package/detectors/openai.js +16 -0
- package/detectors/privateKey.js +16 -0
- package/detectors/registry.js +37 -0
- package/detectors/sendgrid.js +16 -0
- package/detectors/slack.js +16 -0
- package/detectors/stripe.js +16 -0
- package/detectors/telegram.js +16 -0
- package/detectors/twilio.js +16 -0
- package/fixtures/benchmark/negative.json +12 -0
- package/fixtures/benchmark/positive.json +18 -0
- package/package.json +67 -0
- package/scripts/benchmark.js +76 -0
- package/templates/expressProxy.js +40 -0
- package/utils/config.js +109 -0
- package/utils/detectorRunner.js +13 -0
- package/utils/fileScanner.js +74 -0
- package/utils/gitScanner.js +26 -0
- package/utils/logger.js +32 -0
- package/utils/riskAnalyzer.js +42 -0
- package/utils/sarif.js +55 -0
- package/utils/verifier.js +73 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
|
|
6
|
+
const providerConfig = {
|
|
7
|
+
openai: {
|
|
8
|
+
keyName: "OPENAI_API_KEY",
|
|
9
|
+
url: "https://platform.openai.com/api-keys",
|
|
10
|
+
validator: /^sk-(?:proj-)?[A-Za-z0-9_-]{20,}$/,
|
|
11
|
+
},
|
|
12
|
+
google: {
|
|
13
|
+
keyName: "GOOGLE_API_KEY",
|
|
14
|
+
url: "https://console.cloud.google.com/apis/credentials",
|
|
15
|
+
validator: /^AIza[A-Za-z0-9_-]{35}$/,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export async function runRotate(provider, options = {}) {
|
|
20
|
+
const normalized = String(provider || "").toLowerCase();
|
|
21
|
+
const cfg = providerConfig[normalized];
|
|
22
|
+
|
|
23
|
+
if (!cfg) {
|
|
24
|
+
logger.error(`Unsupported provider: ${provider}. Supported: ${Object.keys(providerConfig).join(", ")}`);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
logger.log(`[1] Open: ${cfg.url}`);
|
|
29
|
+
logger.log("[2] Create a new key with least privilege");
|
|
30
|
+
logger.log("[3] Rotate without committing plaintext keys\n");
|
|
31
|
+
|
|
32
|
+
const newKey = await resolveKeyInput(cfg.keyName, options);
|
|
33
|
+
if (!cfg.validator.test(newKey)) {
|
|
34
|
+
logger.error("Key format looks invalid. Aborting .env update.");
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const projectPath = path.resolve(options.path || process.cwd());
|
|
39
|
+
const envPath = options.envFile ? path.resolve(options.envFile) : path.join(projectPath, ".env");
|
|
40
|
+
|
|
41
|
+
let envContent = "";
|
|
42
|
+
try {
|
|
43
|
+
envContent = await fs.readFile(envPath, "utf8");
|
|
44
|
+
} catch {
|
|
45
|
+
envContent = "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const updated = upsertEnv(envContent, cfg.keyName, newKey);
|
|
49
|
+
|
|
50
|
+
if (options.dryRun) {
|
|
51
|
+
logger.warn("Dry run enabled: no file was modified.");
|
|
52
|
+
logger.log(`Would update ${envPath} with ${cfg.keyName}`);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const backupPath = `${envPath}.bak.${Date.now()}`;
|
|
57
|
+
try {
|
|
58
|
+
if (envContent) {
|
|
59
|
+
await fs.writeFile(backupPath, envContent, "utf8");
|
|
60
|
+
logger.log(`Backup created: ${backupPath}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await fs.writeFile(envPath, updated, "utf8");
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (envContent) {
|
|
66
|
+
try {
|
|
67
|
+
await fs.writeFile(envPath, envContent, "utf8");
|
|
68
|
+
} catch {
|
|
69
|
+
logger.error("Rollback failed. Please restore from backup manually.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
logger.error(`Failed to update env file: ${error.message}`);
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
logger.safe(`Updated ${envPath} with ${cfg.keyName}`);
|
|
77
|
+
logger.warn("Restart backend workers and redeploy to activate the new key.");
|
|
78
|
+
logger.log("Run `fixyoursecret scan` (or `secretlint scan`) to verify no hardcoded keys remain.");
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function resolveKeyInput(keyName, options) {
|
|
83
|
+
if (options.key) return String(options.key).trim();
|
|
84
|
+
|
|
85
|
+
if (!input.isTTY) {
|
|
86
|
+
const chunks = [];
|
|
87
|
+
for await (const chunk of input) chunks.push(chunk);
|
|
88
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return readHiddenInput(`New ${keyName}: `);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function readHiddenInput(promptText) {
|
|
95
|
+
output.write(promptText);
|
|
96
|
+
|
|
97
|
+
input.setRawMode(true);
|
|
98
|
+
input.resume();
|
|
99
|
+
input.setEncoding("utf8");
|
|
100
|
+
|
|
101
|
+
return await new Promise((resolve) => {
|
|
102
|
+
let value = "";
|
|
103
|
+
|
|
104
|
+
function onData(char) {
|
|
105
|
+
if (char === "\r" || char === "\n") {
|
|
106
|
+
output.write("\n");
|
|
107
|
+
cleanup();
|
|
108
|
+
resolve(value.trim());
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (char === "\u0003") {
|
|
112
|
+
cleanup();
|
|
113
|
+
process.exit(130);
|
|
114
|
+
}
|
|
115
|
+
if (char === "\u007f") {
|
|
116
|
+
value = value.slice(0, -1);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
value += char;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cleanup() {
|
|
123
|
+
input.off("data", onData);
|
|
124
|
+
input.setRawMode(false);
|
|
125
|
+
input.pause();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
input.on("data", onData);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function upsertEnv(envContent, key, value) {
|
|
133
|
+
const lineRegex = new RegExp(`^${escapeRegExp(key)}=.*$`, "m");
|
|
134
|
+
const entry = `${key}=${value}`;
|
|
135
|
+
|
|
136
|
+
if (lineRegex.test(envContent)) {
|
|
137
|
+
return envContent.replace(lineRegex, entry);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (envContent.length === 0) return `${entry}\n`;
|
|
141
|
+
return envContent.endsWith("\n") ? envContent + `${entry}\n` : `${envContent}\n${entry}\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function escapeRegExp(value) {
|
|
145
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
146
|
+
}
|
package/commands/scan.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { collectProjectFiles, lineColFromIndex } from "../utils/fileScanner.js";
|
|
4
|
+
import { analyzeRisk } from "../utils/riskAnalyzer.js";
|
|
5
|
+
import { printFinding, printSummary, logger } from "../utils/logger.js";
|
|
6
|
+
import { findingsToSarif } from "../utils/sarif.js";
|
|
7
|
+
import { getRecentChangedFiles, getStagedFiles, getTrackedFiles } from "../utils/gitScanner.js";
|
|
8
|
+
import { isSuppressed, loadConfig, resolveBaselinePath } from "../utils/config.js";
|
|
9
|
+
import { normalizeVerifyMode, shouldSkipAsNonSecret, verifyFinding } from "../utils/verifier.js";
|
|
10
|
+
import { runDetectors } from "../utils/detectorRunner.js";
|
|
11
|
+
|
|
12
|
+
const SCORE = { LOW: 1, MEDIUM: 2, HIGH: 3 };
|
|
13
|
+
|
|
14
|
+
export async function runScan(options = {}) {
|
|
15
|
+
const projectPath = path.resolve(options.path || process.cwd());
|
|
16
|
+
const cfgLoad = await loadConfig(projectPath, options.config);
|
|
17
|
+
const config = cfgLoad.config;
|
|
18
|
+
|
|
19
|
+
const verifyMode = normalizeVerifyMode(options.verify || config.verifyMode || "none");
|
|
20
|
+
const verifyStrict = Boolean(options.verifyStrict);
|
|
21
|
+
|
|
22
|
+
const includeOnly = await resolveScanScope(projectPath, options);
|
|
23
|
+
|
|
24
|
+
const files = await collectProjectFiles(projectPath, {
|
|
25
|
+
allowedExtensions: config.allowedExtensions,
|
|
26
|
+
ignorePaths: config.ignorePaths,
|
|
27
|
+
maxFileSizeKB: config.maxFileSizeKB,
|
|
28
|
+
includeOnly,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const findings = [];
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const matches = runDetectors(file.content, config);
|
|
35
|
+
|
|
36
|
+
for (const match of matches) {
|
|
37
|
+
const { line, column } = lineColFromIndex(file.content, match.index);
|
|
38
|
+
const snippet = file.lines[line - 1]?.trim() || "";
|
|
39
|
+
|
|
40
|
+
if (shouldSkipAsNonSecret(match, snippet, file.relativePath, config.ignoreValueHints)) continue;
|
|
41
|
+
|
|
42
|
+
const risk = analyzeRisk(file.relativePath, match, snippet);
|
|
43
|
+
const finding = {
|
|
44
|
+
file: file.relativePath,
|
|
45
|
+
line,
|
|
46
|
+
column,
|
|
47
|
+
issue: match.issue,
|
|
48
|
+
rule: match.rule,
|
|
49
|
+
severity: risk.severity,
|
|
50
|
+
reason: risk.reason,
|
|
51
|
+
snippet: snippet.slice(0, 180),
|
|
52
|
+
recommendation: risk.fix,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (verifyMode !== "none") {
|
|
56
|
+
const verification = verifyFinding(match, file.content, snippet);
|
|
57
|
+
finding.verified = verification.verified;
|
|
58
|
+
finding.verificationMethod = verification.verificationMethod;
|
|
59
|
+
if (verifyStrict && !verification.verified) continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (isSuppressed(finding, config.suppressions, file.lines)) continue;
|
|
63
|
+
findings.push(finding);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const deduped = dedupeFindings(findings);
|
|
68
|
+
const filtered = await applyBaselineFilter(projectPath, deduped, options);
|
|
69
|
+
|
|
70
|
+
await maybeUpdateBaseline(projectPath, deduped, options);
|
|
71
|
+
|
|
72
|
+
const format = normalizeFormat(options.format, options.json);
|
|
73
|
+
await emitOutput(filtered, format, options.outputFile);
|
|
74
|
+
|
|
75
|
+
if (format === "text") {
|
|
76
|
+
if (filtered.length === 0) {
|
|
77
|
+
logger.safe("No leaked secrets detected.");
|
|
78
|
+
} else {
|
|
79
|
+
for (const finding of filtered) printFinding(finding);
|
|
80
|
+
printSummary(filtered);
|
|
81
|
+
}
|
|
82
|
+
if (cfgLoad.loaded) logger.log(`Config: ${cfgLoad.path}`);
|
|
83
|
+
if (verifyMode !== "none") logger.log(`Verification mode: ${verifyMode}${verifyStrict ? " (strict)" : ""}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return shouldFail(filtered, options.failOn || config.failOn || "high") ? 1 : 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function resolveScanScope(projectPath, options) {
|
|
90
|
+
if (options.staged) return getStagedFiles(projectPath);
|
|
91
|
+
if (options.tracked) return getTrackedFiles(projectPath);
|
|
92
|
+
if (options.history) return getRecentChangedFiles(projectPath, Number(options.history));
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function dedupeFindings(findings) {
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
const out = [];
|
|
99
|
+
|
|
100
|
+
for (const f of findings) {
|
|
101
|
+
const key = `${f.file}:${f.line}:${f.rule}:${f.snippet}`;
|
|
102
|
+
if (!seen.has(key)) {
|
|
103
|
+
seen.add(key);
|
|
104
|
+
out.push(f);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function applyBaselineFilter(projectPath, findings, options) {
|
|
112
|
+
const candidates = [
|
|
113
|
+
resolveBaselinePath(projectPath, options.baseline),
|
|
114
|
+
path.resolve(projectPath, ".secretlint-baseline.json"),
|
|
115
|
+
];
|
|
116
|
+
if (options.noBaseline) return findings;
|
|
117
|
+
|
|
118
|
+
let baselineSet = null;
|
|
119
|
+
for (const baselinePath of candidates) {
|
|
120
|
+
try {
|
|
121
|
+
const raw = await fs.readFile(baselinePath, "utf8");
|
|
122
|
+
const list = JSON.parse(raw);
|
|
123
|
+
if (Array.isArray(list)) {
|
|
124
|
+
baselineSet = new Set(list.filter((v) => typeof v === "string"));
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// keep checking fallback baseline
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!baselineSet) return findings;
|
|
133
|
+
return findings.filter((f) => !baselineSet.has(fingerprint(f)));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function maybeUpdateBaseline(projectPath, findings, options) {
|
|
137
|
+
if (!options.updateBaseline) return;
|
|
138
|
+
const baselinePath = resolveBaselinePath(projectPath, options.baseline);
|
|
139
|
+
const entries = findings.map((f) => fingerprint(f)).sort();
|
|
140
|
+
await fs.writeFile(baselinePath, JSON.stringify(entries, null, 2) + "\n", "utf8");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function emitOutput(findings, format, outputFile) {
|
|
144
|
+
if (format === "json") {
|
|
145
|
+
const payload = JSON.stringify(findings, null, 2);
|
|
146
|
+
return writeOrPrint(payload, outputFile);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (format === "sarif") {
|
|
150
|
+
const payload = JSON.stringify(findingsToSarif(findings, "fixyoursecret"), null, 2);
|
|
151
|
+
return writeOrPrint(payload, outputFile);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function writeOrPrint(payload, outputFile) {
|
|
156
|
+
if (outputFile) {
|
|
157
|
+
await fs.writeFile(path.resolve(outputFile), payload + "\n", "utf8");
|
|
158
|
+
} else {
|
|
159
|
+
logger.log(payload);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeFormat(format, jsonFlag) {
|
|
164
|
+
if (jsonFlag) return "json";
|
|
165
|
+
const safe = String(format || "text").toLowerCase();
|
|
166
|
+
return ["text", "json", "sarif"].includes(safe) ? safe : "text";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function shouldFail(findings, failOn) {
|
|
170
|
+
const threshold = toSeverityLabel(failOn);
|
|
171
|
+
const thresholdScore = SCORE[threshold];
|
|
172
|
+
return findings.some((f) => SCORE[f.severity] >= thresholdScore);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toSeverityLabel(value) {
|
|
176
|
+
const safe = String(value || "high").toUpperCase();
|
|
177
|
+
if (safe === "LOW") return "LOW";
|
|
178
|
+
if (safe === "MEDIUM") return "MEDIUM";
|
|
179
|
+
return "HIGH";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function fingerprint(f) {
|
|
183
|
+
return `${f.file}:${f.line}:${f.rule}:${f.snippet}`;
|
|
184
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const ANTHROPIC_REGEX = /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g;
|
|
2
|
+
|
|
3
|
+
export function detectAnthropic(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(ANTHROPIC_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "anthropic-api-key",
|
|
8
|
+
issue: "Anthropic API key exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "anthropic",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
package/detectors/aws.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const AWS_ACCESS_KEY_ID_REGEX = /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g;
|
|
2
|
+
|
|
3
|
+
export function detectAWS(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(AWS_ACCESS_KEY_ID_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "aws-access-key-id",
|
|
8
|
+
issue: "AWS access key ID exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "aws",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const COHERE_REGEX = /\bco_[A-Za-z0-9]{30,}\b/g;
|
|
2
|
+
|
|
3
|
+
export function detectCohere(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(COHERE_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "cohere-api-key",
|
|
8
|
+
issue: "Cohere API key exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "cohere",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const TOKEN_REGEX = /[A-Za-z0-9_\-]{24,}/g;
|
|
2
|
+
|
|
3
|
+
export function detectGenericSecrets(content, options = {}) {
|
|
4
|
+
const threshold = Number.isFinite(options.entropyThreshold) ? options.entropyThreshold : 3.8;
|
|
5
|
+
const findings = [];
|
|
6
|
+
for (const match of content.matchAll(TOKEN_REGEX)) {
|
|
7
|
+
const value = match[0];
|
|
8
|
+
if (!looksLikeHighEntropy(value, threshold)) continue;
|
|
9
|
+
if (looksSafeCommonWord(value)) continue;
|
|
10
|
+
|
|
11
|
+
findings.push({
|
|
12
|
+
rule: "generic-high-entropy",
|
|
13
|
+
issue: "Potential secret-like token detected",
|
|
14
|
+
index: match.index ?? 0,
|
|
15
|
+
value,
|
|
16
|
+
type: "generic",
|
|
17
|
+
confidence: "medium",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return findings;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function looksLikeHighEntropy(value, threshold) {
|
|
24
|
+
const entropy = shannonEntropy(value);
|
|
25
|
+
const hasLower = /[a-z]/.test(value);
|
|
26
|
+
const hasUpperOrSymbol = /[A-Z]/.test(value) || /[_-]/.test(value);
|
|
27
|
+
const hasDigit = /\d/.test(value);
|
|
28
|
+
return entropy >= threshold && hasLower && hasUpperOrSymbol && hasDigit;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function looksSafeCommonWord(value) {
|
|
32
|
+
return (
|
|
33
|
+
value.startsWith("sk-") ||
|
|
34
|
+
value.startsWith("AIza") ||
|
|
35
|
+
value.startsWith("pk_test_") ||
|
|
36
|
+
value.startsWith("pk_live_") ||
|
|
37
|
+
value.startsWith("http") ||
|
|
38
|
+
value.includes("localhost") ||
|
|
39
|
+
value.toLowerCase().includes("component") ||
|
|
40
|
+
value.toLowerCase().includes("configuration")
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function shannonEntropy(value) {
|
|
45
|
+
const map = new Map();
|
|
46
|
+
for (const ch of value) {
|
|
47
|
+
map.set(ch, (map.get(ch) || 0) + 1);
|
|
48
|
+
}
|
|
49
|
+
let entropy = 0;
|
|
50
|
+
for (const count of map.values()) {
|
|
51
|
+
const p = count / value.length;
|
|
52
|
+
entropy -= p * Math.log2(p);
|
|
53
|
+
}
|
|
54
|
+
return entropy;
|
|
55
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const GITHUB_TOKEN_REGEX = /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b|\bgithub_pat_[A-Za-z0-9_]{20,}\b/g;
|
|
2
|
+
|
|
3
|
+
export function detectGitHub(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(GITHUB_TOKEN_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "github-token",
|
|
8
|
+
issue: "GitHub token exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "github",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const GITLAB_REGEX = /\bglpat-[A-Za-z0-9_-]{20,}\b/g;
|
|
2
|
+
|
|
3
|
+
export function detectGitLab(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(GITLAB_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "gitlab-token",
|
|
8
|
+
issue: "GitLab token exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "gitlab",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const GOOGLE_REGEX = /AIza[A-Za-z0-9_-]{35}/g;
|
|
2
|
+
|
|
3
|
+
export function detectGoogle(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(GOOGLE_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "google-key",
|
|
8
|
+
issue: "Google API key exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "google",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const HF_REGEX = /\bhf_[A-Za-z0-9]{30,}\b/g;
|
|
2
|
+
|
|
3
|
+
export function detectHuggingFace(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(HF_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "huggingface-token",
|
|
8
|
+
issue: "Hugging Face token exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "huggingface",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const MAILGUN_REGEX = /\bkey-[A-Za-z0-9]{32}\b/g;
|
|
2
|
+
|
|
3
|
+
export function detectMailgun(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(MAILGUN_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "mailgun-api-key",
|
|
8
|
+
issue: "Mailgun API key exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "mailgun",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const NPM_REGEX = /\bnpm_[A-Za-z0-9]{36}\b/g;
|
|
2
|
+
|
|
3
|
+
export function detectNpmToken(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(NPM_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "npm-token",
|
|
8
|
+
issue: "npm token exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "npm",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const OPENAI_REGEX = /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g;
|
|
2
|
+
|
|
3
|
+
export function detectOpenAI(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(OPENAI_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "openai-key",
|
|
8
|
+
issue: "OpenAI key exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "openai",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const PRIVATE_KEY_REGEX = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g;
|
|
2
|
+
|
|
3
|
+
export function detectPrivateKey(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(PRIVATE_KEY_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "private-key-block",
|
|
8
|
+
issue: "Private key material exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "private-key",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { detectOpenAI } from "./openai.js";
|
|
2
|
+
import { detectGoogle } from "./google.js";
|
|
3
|
+
import { detectAWS } from "./aws.js";
|
|
4
|
+
import { detectStripe } from "./stripe.js";
|
|
5
|
+
import { detectSlack } from "./slack.js";
|
|
6
|
+
import { detectGitHub } from "./github.js";
|
|
7
|
+
import { detectPrivateKey } from "./privateKey.js";
|
|
8
|
+
import { detectGenericSecrets } from "./generic.js";
|
|
9
|
+
import { detectTwilio } from "./twilio.js";
|
|
10
|
+
import { detectSendGrid } from "./sendgrid.js";
|
|
11
|
+
import { detectMailgun } from "./mailgun.js";
|
|
12
|
+
import { detectAnthropic } from "./anthropic.js";
|
|
13
|
+
import { detectCohere } from "./cohere.js";
|
|
14
|
+
import { detectHuggingFace } from "./huggingface.js";
|
|
15
|
+
import { detectTelegram } from "./telegram.js";
|
|
16
|
+
import { detectNpmToken } from "./npmToken.js";
|
|
17
|
+
import { detectGitLab } from "./gitlab.js";
|
|
18
|
+
|
|
19
|
+
export const DETECTOR_REGISTRY = [
|
|
20
|
+
{ key: "openai", run: detectOpenAI },
|
|
21
|
+
{ key: "google", run: detectGoogle },
|
|
22
|
+
{ key: "aws", run: detectAWS },
|
|
23
|
+
{ key: "stripe", run: detectStripe },
|
|
24
|
+
{ key: "slack", run: detectSlack },
|
|
25
|
+
{ key: "github", run: detectGitHub },
|
|
26
|
+
{ key: "gitlab", run: detectGitLab },
|
|
27
|
+
{ key: "twilio", run: detectTwilio },
|
|
28
|
+
{ key: "sendgrid", run: detectSendGrid },
|
|
29
|
+
{ key: "mailgun", run: detectMailgun },
|
|
30
|
+
{ key: "anthropic", run: detectAnthropic },
|
|
31
|
+
{ key: "cohere", run: detectCohere },
|
|
32
|
+
{ key: "huggingface", run: detectHuggingFace },
|
|
33
|
+
{ key: "telegram", run: detectTelegram },
|
|
34
|
+
{ key: "npm", run: detectNpmToken },
|
|
35
|
+
{ key: "private-key", run: detectPrivateKey },
|
|
36
|
+
{ key: "generic", run: (content, options) => detectGenericSecrets(content, options) },
|
|
37
|
+
];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const SENDGRID_REGEX = /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/g;
|
|
2
|
+
|
|
3
|
+
export function detectSendGrid(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(SENDGRID_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "sendgrid-api-key",
|
|
8
|
+
issue: "SendGrid API key exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "sendgrid",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const SLACK_TOKEN_REGEX = /\bxox(?:b|p|a|r|s)-[0-9A-Za-z-]{10,}\b/g;
|
|
2
|
+
|
|
3
|
+
export function detectSlack(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(SLACK_TOKEN_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "slack-token",
|
|
8
|
+
issue: "Slack token exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "slack",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const STRIPE_SECRET_REGEX = /\bsk_live_[0-9A-Za-z]{16,}\b/g;
|
|
2
|
+
|
|
3
|
+
export function detectStripe(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(STRIPE_SECRET_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "stripe-secret-key",
|
|
8
|
+
issue: "Stripe secret key exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "stripe",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const TELEGRAM_REGEX = /\b\d{8,10}:[A-Za-z0-9_-]{35}\b/g;
|
|
2
|
+
|
|
3
|
+
export function detectTelegram(content) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const match of content.matchAll(TELEGRAM_REGEX)) {
|
|
6
|
+
findings.push({
|
|
7
|
+
rule: "telegram-bot-token",
|
|
8
|
+
issue: "Telegram bot token exposed",
|
|
9
|
+
index: match.index ?? 0,
|
|
10
|
+
value: match[0],
|
|
11
|
+
type: "telegram",
|
|
12
|
+
confidence: "high",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return findings;
|
|
16
|
+
}
|