@masyv/secretscan 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MASYV
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # SecretScan
2
+
3
+ **Real-time secret & credential detector for Claude Code.** Blocks API keys, tokens, private keys, and database passwords from ever entering your LLM context window.
4
+
5
+ [![Rust](https://img.shields.io/badge/Rust-1.70+-orange.svg)](https://www.rust-lang.org/)
6
+ [![npm](https://img.shields.io/npm/v/@masyv/secretscan)](https://www.npmjs.com/package/@masyv/secretscan)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+
9
+ ## The Problem
10
+
11
+ When Claude Code runs tools, their outputs flow directly into the context window. That includes:
12
+ - `cat .env` → your API keys
13
+ - `git log` → commit messages with accidentally committed credentials
14
+ - Database query results → connection strings, hashed passwords
15
+ - `curl` responses → tokens, JWTs, session cookies
16
+
17
+ **SecretScan intercepts every tool output** before it reaches Claude, redacts detected secrets, and logs them locally. Claude sees `[REDACTED:anthropic_api_key:bbfc6912]` — not `sk-ant-api03-...`.
18
+
19
+ ## 47 Built-in Patterns
20
+
21
+ | Severity | Providers |
22
+ |----------|-----------|
23
+ | 🔴 Critical | Anthropic, OpenAI, AWS, GitHub, Stripe, Google Service Account, PostgreSQL, MySQL, MongoDB, PEM Private Keys |
24
+ | 🟠 High | GitLab, Slack, npm, SendGrid, Cloudflare, Azure, Redis, Heroku, Vercel, Datadog, HuggingFace, Discord, Shopify |
25
+ | 🟡 Medium | JWT tokens, Twilio, env file secrets, Slack webhooks |
26
+ | 🔵 Low | Test keys, certificates, high-entropy strings |
27
+
28
+ Plus **Shannon entropy analysis** for detecting unknown secrets by statistical pattern.
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ # Install from source
34
+ git clone https://github.com/Manavarya09/secretscan
35
+ cd secretscan
36
+ ./scripts/build.sh && ./scripts/install.sh
37
+
38
+ # Auto-configure Claude Code
39
+ secretscan setup
40
+
41
+ # That's it — restart Claude Code and you're protected.
42
+ ```
43
+
44
+ ## What It Looks Like
45
+
46
+ ```bash
47
+ $ echo 'ANTHROPIC_API_KEY=sk-ant-api03-...' | secretscan scan
48
+
49
+ 🚨 1 secret found:
50
+
51
+ 🔴 [CRITICAL] Anthropic API Key fingerprint: bbfc6912
52
+ sk-ant-api03-xxxxx…
53
+ ```
54
+
55
+ With the hook active, Claude sees:
56
+ ```
57
+ ANTHROPIC_API_KEY=[REDACTED:anthropic_api_key:bbfc6912]
58
+ ```
59
+
60
+ ## Recovery
61
+
62
+ Originals are stored **locally** in SQLite — never forwarded anywhere:
63
+
64
+ ```bash
65
+ secretscan expand bbfc6912
66
+ # → sk-ant-api03-...
67
+ ```
68
+
69
+ ## Allowlist
70
+
71
+ False positive? Mark it safe:
72
+
73
+ ```bash
74
+ secretscan allow bbfc6912 --reason "This is a test key in the repo"
75
+ ```
76
+
77
+ ## CLI Reference
78
+
79
+ ```
80
+ COMMANDS:
81
+ scan Scan text, file, or stdin for secrets
82
+ hook PostToolUse hook mode (reads hook JSON from stdin)
83
+ expand Retrieve original value by fingerprint
84
+ allow Add fingerprint to allowlist
85
+ unallow Remove fingerprint from allowlist
86
+ stats Show scan statistics
87
+ audit List recent findings
88
+ patterns List all 47 built-in patterns
89
+ setup Auto-install PostToolUse hook into ~/.claude/settings.json
90
+
91
+ OPTIONS:
92
+ --json Output as JSON
93
+ --db-path SQLite path [default: ~/.secretscan/secretscan.db]
94
+ -v, --verbose Verbose logging
95
+ ```
96
+
97
+ ## Manual Hook Setup
98
+
99
+ Add to `~/.claude/settings.json`:
100
+
101
+ ```json
102
+ {
103
+ "hooks": {
104
+ "PostToolUse": [
105
+ {
106
+ "matcher": "*",
107
+ "hooks": [{ "type": "command", "command": "secretscan hook" }]
108
+ }
109
+ ]
110
+ }
111
+ }
112
+ ```
113
+
114
+ ## Performance
115
+
116
+ - **< 2ms** per scan for typical tool outputs
117
+ - **Zero network calls** — everything is local
118
+ - **< 5MB** binary (release, stripped)
119
+
120
+ ## License
121
+
122
+ MIT
@@ -0,0 +1,51 @@
1
+ [package]
2
+ name = "secretscan"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "SecretScan — Real-time secret & credential detector for Claude Code. Blocks API keys, tokens, and private keys from entering your LLM context."
6
+ license = "MIT"
7
+
8
+ [[bin]]
9
+ name = "secretscan"
10
+ path = "src/main.rs"
11
+
12
+ [lib]
13
+ name = "secretscan"
14
+ path = "src/lib.rs"
15
+
16
+ [dependencies]
17
+ # CLI
18
+ clap = { version = "4", features = ["derive"] }
19
+
20
+ # Serialization
21
+ serde = { version = "1", features = ["derive"] }
22
+ serde_json = "1"
23
+
24
+ # Error handling
25
+ anyhow = "1"
26
+ thiserror = "2"
27
+
28
+ # Regex
29
+ regex = "1"
30
+ once_cell = "1"
31
+
32
+ # SQLite (lossless local storage of redacted originals)
33
+ rusqlite = { version = "0.32", features = ["bundled"] }
34
+
35
+ # Hashing
36
+ blake3 = "1"
37
+
38
+ # Logging
39
+ tracing = "0.1"
40
+ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
41
+
42
+ # Time
43
+ chrono = { version = "0.4", features = ["serde"] }
44
+
45
+ # Colors for terminal output
46
+ colored = "2"
47
+
48
+ [profile.release]
49
+ opt-level = 3
50
+ lto = "thin"
51
+ strip = true
@@ -0,0 +1,100 @@
1
+ //! Claude Code hook handler.
2
+ //! Reads PostToolUse / PreToolUse JSON from stdin, redacts secrets, writes to stdout.
3
+
4
+ use anyhow::Result;
5
+ use serde::{Deserialize, Serialize};
6
+ use std::path::Path;
7
+
8
+ use crate::redact;
9
+
10
+ /// Claude Code PostToolUse hook input format.
11
+ #[derive(Debug, Deserialize)]
12
+ pub struct HookInput {
13
+ #[serde(default)]
14
+ pub tool_name: String,
15
+ #[serde(default)]
16
+ pub tool_input: serde_json::Value,
17
+ #[serde(default)]
18
+ pub tool_output: String,
19
+ }
20
+
21
+ /// Hook output — same shape as input with redacted fields.
22
+ #[derive(Debug, Serialize)]
23
+ pub struct HookOutput {
24
+ pub tool_name: String,
25
+ pub tool_input: serde_json::Value,
26
+ pub tool_output: String,
27
+ #[serde(skip_serializing_if = "Option::is_none")]
28
+ pub secretscan: Option<HookMeta>,
29
+ }
30
+
31
+ #[derive(Debug, Serialize)]
32
+ pub struct HookMeta {
33
+ pub secrets_found: usize,
34
+ pub redacted: Vec<String>,
35
+ pub clean: bool,
36
+ }
37
+
38
+ /// Process a hook call: scan + redact tool_output and tool_input values.
39
+ pub fn process(raw: &str, db_path: &Path, session_id: &str) -> Result<String> {
40
+ let input: HookInput = match serde_json::from_str(raw) {
41
+ Ok(v) => v,
42
+ Err(_) => {
43
+ // Not a valid hook payload — pass through unchanged
44
+ return Ok(raw.to_string());
45
+ }
46
+ };
47
+
48
+ let tool_name = input.tool_name.as_str();
49
+
50
+ // Redact tool_output
51
+ let output_result = redact::scan_and_redact(
52
+ &input.tool_output,
53
+ Some(tool_name),
54
+ db_path,
55
+ session_id,
56
+ );
57
+
58
+ // Redact tool_input (serialized to string for scanning)
59
+ let input_str = serde_json::to_string(&input.tool_input).unwrap_or_default();
60
+ let input_result = redact::scan_and_redact(
61
+ &input_str,
62
+ Some(tool_name),
63
+ db_path,
64
+ session_id,
65
+ );
66
+ let redacted_input: serde_json::Value =
67
+ serde_json::from_str(&input_result.redacted_text).unwrap_or(input.tool_input);
68
+
69
+ let total_found = output_result.findings.len() + input_result.findings.len();
70
+ let mut all_redacted: Vec<String> = output_result
71
+ .findings
72
+ .iter()
73
+ .map(|f| format!("{} ({})", f.pattern_name, f.severity.as_str()))
74
+ .collect();
75
+ all_redacted.extend(
76
+ input_result
77
+ .findings
78
+ .iter()
79
+ .map(|f| format!("{} ({})", f.pattern_name, f.severity.as_str())),
80
+ );
81
+
82
+ let meta = if total_found > 0 {
83
+ Some(HookMeta {
84
+ secrets_found: total_found,
85
+ redacted: all_redacted,
86
+ clean: false,
87
+ })
88
+ } else {
89
+ None
90
+ };
91
+
92
+ let output = HookOutput {
93
+ tool_name: tool_name.to_string(),
94
+ tool_input: redacted_input,
95
+ tool_output: output_result.redacted_text,
96
+ secretscan: meta,
97
+ };
98
+
99
+ Ok(serde_json::to_string(&output)?)
100
+ }
@@ -0,0 +1,93 @@
1
+ pub mod hook;
2
+ pub mod patterns;
3
+ pub mod redact;
4
+ pub mod store;
5
+
6
+ use serde::{Deserialize, Serialize};
7
+
8
+ /// Secret severity level.
9
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
10
+ #[serde(rename_all = "lowercase")]
11
+ pub enum Severity {
12
+ Low,
13
+ Medium,
14
+ High,
15
+ Critical,
16
+ }
17
+
18
+ impl Severity {
19
+ pub fn as_str(self) -> &'static str {
20
+ match self {
21
+ Self::Low => "low",
22
+ Self::Medium => "medium",
23
+ Self::High => "high",
24
+ Self::Critical => "critical",
25
+ }
26
+ }
27
+
28
+ pub fn emoji(self) -> &'static str {
29
+ match self {
30
+ Self::Low => "🔵",
31
+ Self::Medium => "🟡",
32
+ Self::High => "🟠",
33
+ Self::Critical => "🔴",
34
+ }
35
+ }
36
+
37
+ pub fn color_code(self) -> &'static str {
38
+ match self {
39
+ Self::Low => "\x1b[34m", // blue
40
+ Self::Medium => "\x1b[33m", // yellow
41
+ Self::High => "\x1b[91m", // bright red
42
+ Self::Critical => "\x1b[31m", // red
43
+ }
44
+ }
45
+ }
46
+
47
+ /// A single secret detection finding.
48
+ #[derive(Debug, Clone, Serialize, Deserialize)]
49
+ pub struct Finding {
50
+ pub pattern_id: &'static str,
51
+ pub pattern_name: &'static str,
52
+ pub severity: Severity,
53
+ /// The original matched string (stored locally, never forwarded).
54
+ pub matched: String,
55
+ /// Safe replacement string: `[REDACTED:pattern_id:fingerprint]`
56
+ pub redacted: String,
57
+ /// First 8 hex chars of blake3(matched) — used for recovery + allowlist.
58
+ pub fingerprint: String,
59
+ pub offset: usize,
60
+ pub length: usize,
61
+ }
62
+
63
+ /// Result of a scan + redact operation.
64
+ #[derive(Debug, Serialize)]
65
+ pub struct ScanResult {
66
+ pub original_len: usize,
67
+ pub redacted_text: String,
68
+ pub findings: Vec<Finding>,
69
+ pub clean: bool,
70
+ }
71
+
72
+ impl ScanResult {
73
+ pub fn summary(&self) -> String {
74
+ if self.clean {
75
+ return "✅ No secrets detected.".to_string();
76
+ }
77
+ let mut lines = vec![format!(
78
+ "🚨 {} secret{} found:",
79
+ self.findings.len(),
80
+ if self.findings.len() == 1 { "" } else { "s" }
81
+ )];
82
+ for f in &self.findings {
83
+ lines.push(format!(
84
+ " {} [{}] {} — {}",
85
+ f.severity.emoji(),
86
+ f.severity.as_str().to_uppercase(),
87
+ f.pattern_name,
88
+ f.fingerprint
89
+ ));
90
+ }
91
+ lines.join("\n")
92
+ }
93
+ }