A secret scanner should be useful before a secret reaches Git history, not just after a security incident. I built the API Secret Scanner as a deliberately small client-side analyzer: paste code or upload a text file, scan each line against known token shapes, and show a masked finding with its line number. That workflow is fast enough for a pre-commit spot check and transparent enough to inspect.
The primary reader is a developer reviewing an .env, configuration file, or pull request who needs to understand what a pattern-based scanner can and cannot promise. The takeaway is not “regex proves a credential is active.” It is how to design a useful first-pass detector while protecting the value it found. The tool itself is the example, not a substitute for revocation, history cleanup, or a full secret-management system.
The pattern table makes policy visible
The implementation stores recognizers with a type and severity. High-severity examples include AWS access key IDs, private-key headers, JWTs, and Stripe live keys:
const SECRET_PATTERNS = [
{ type: "AWS Access Key ID", severity: "HIGH", re: /\bAKIA[0-9A-Z]{16}\b/ },
{ type: "RSA/EC/OPENSSH Private Key", severity: "HIGH",
re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/ },
{ type: "JWT Token", severity: "HIGH",
re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
{ type: "GitHub PAT (ghp_)", severity: "MEDIUM",
re: /\bghp_[A-Za-z0-9]{36,}\b/ },
{ type: "Hardcoded token=", severity: "LOW",
re: /(?:access_token|auth_token)\s*[=:]\s*["'][^"']{16,}["']/i },
];
The full source includes provider-specific formats for GitHub, GitLab, Google, Slack, SendGrid, Twilio, Stripe, npm, PyPI, Shopify, Azure, Firebase, Telegram, Discord, and more, plus generic hardcoded password, secret, API-key, token, and basic-auth patterns. Keeping these as data makes it possible to audit the policy instead of hiding every decision inside one enormous expression.
Scan line by line and preserve only a masked preview
scanText splits on newline, tests each pattern against the current line, and records the first match returned by that pattern:
function scanText(text) {
const lines = text.split("\n");
const result = [];
for (let i = 0; i < lines.length; i++) {
for (const pattern of SECRET_PATTERNS) {
const match = lines[i].match(pattern.re);
if (match) {
const raw = match[1] || match[0];
result.push({
type: pattern.type,
severity: pattern.severity || "LOW",
line: i + 1,
masked: maskValue(raw),
});
}
}
}
return result;
}
The output stores the line number and classification, not a copy of the whole source file. The masking function keeps four characters at each end and replaces the middle with a bounded run of asterisks:
function maskValue(val) {
if (val.length <= 8) return "****";
return val.slice(0, 4) +
"*".repeat(Math.min(val.length - 8, 20)) +
val.slice(-4);
}
That is enough to recognize which credential was found without placing the full value in the results table or downloaded report. The report includes the type, severity, line, and masked value, plus a timestamp and summary counts.
Client-side is a useful boundary, not a security guarantee
The file upload uses FileReader.readAsText(file, "utf-8"); the scanner never calls an API or sends the content to a server. The privacy badge is therefore grounded in the implementation. A browser extension, injected script, or compromised machine can still read a page’s memory, so “client-side” should not be confused with a secure vault. I would still rotate a real credential immediately if it appears in a scan.
The severity counts are computed from the findings array:
const counts = computed(() => ({
HIGH: findings.value.filter((f) => f.severity === "HIGH").length,
MEDIUM: findings.value.filter((f) => f.severity === "MEDIUM").length,
LOW: findings.value.filter((f) => f.severity === "LOW").length,
}));
The scan is explicit about where a result came from. A match carries the pattern’s human-readable type, the normalized severity, and i + 1 for a line number, so the review can jump back to the source instead of searching a giant report. After a file upload, the reader places the text into the same inputText ref and clears old findings; uploading a new file does not silently mix results from two files. Clicking Clear resets the text, findings, and scanned flag together.
That reset behavior is a small but important safety property. A user should not see yesterday’s finding labeled as the result of today’s clean file, and an empty input disables the scan button in the template. The report is generated from the current findings only, with new Date().toISOString() making its timestamp unambiguous across time zones.
Where regex reaches its edge
This is static matching. A fake test string with the right shape can be a false positive, while a newly introduced provider format, a split token, an encoded value, or a secret assembled at runtime can be missed. Because the code calls match on each pattern without a global flag, it records at most one match per pattern per line; repeated credentials of the same type on one line are not exhaustively listed. Generic patterns can also flag ordinary configuration values.
Those limitations are why “no known secret patterns detected” is not the same as “safe.” Use the result as a review queue, then inspect context, check repository history, revoke exposed credentials, and add a real pre-commit or CI scanner where the project needs one. I turned this implementation into a small free tool: API Secret Scanner.
Top comments (0)