keysentinel 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/lib/scanner.js ADDED
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ /**
3
+ * Core scanning logic - pure functions for testability
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.extractAddedLines = extractAddedLines;
7
+ exports.scanWithPatterns = scanWithPatterns;
8
+ exports.scanLines = scanLines;
9
+ exports.generateReport = generateReport;
10
+ exports.shouldFail = shouldFail;
11
+ const patterns_1 = require("./patterns");
12
+ const mask_1 = require("./mask");
13
+ const config_1 = require("./config");
14
+ /**
15
+ * Extract added lines from a unified diff patch
16
+ * Returns array of { line: text, lineNumber: number }
17
+ */
18
+ function extractAddedLines(patch) {
19
+ const added = [];
20
+ const lines = patch.split('\n');
21
+ let currentLineNumber = 0;
22
+ for (const line of lines) {
23
+ // Parse hunk header: @@ -old,len +new,len @@
24
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
25
+ if (hunkMatch) {
26
+ currentLineNumber = parseInt(hunkMatch[1], 10) - 1;
27
+ continue;
28
+ }
29
+ // Skip the diff header lines
30
+ if (line.startsWith('+++') || line.startsWith('---')) {
31
+ continue;
32
+ }
33
+ // Added lines start with +
34
+ if (line.startsWith('+')) {
35
+ currentLineNumber++;
36
+ added.push({
37
+ line: line.slice(1), // Remove the leading +
38
+ lineNumber: currentLineNumber,
39
+ });
40
+ }
41
+ else if (line.startsWith('-')) {
42
+ // Deleted lines don't change the line number
43
+ continue;
44
+ }
45
+ else if (line.startsWith(' ') || line === '') {
46
+ // Context lines
47
+ currentLineNumber++;
48
+ }
49
+ }
50
+ return added;
51
+ }
52
+ /**
53
+ * Scan text for secrets using regex patterns
54
+ */
55
+ function scanWithPatterns(text, patterns, allowlist) {
56
+ const matches = [];
57
+ for (const pattern of patterns) {
58
+ // Reset regex state
59
+ pattern.pattern.lastIndex = 0;
60
+ let match;
61
+ while ((match = pattern.pattern.exec(text)) !== null) {
62
+ // Get the captured group or the full match
63
+ const value = match[1] || match[0];
64
+ // Skip if allowlisted
65
+ if ((0, config_1.isAllowlisted)(value, allowlist)) {
66
+ continue;
67
+ }
68
+ matches.push({
69
+ pattern,
70
+ match: value,
71
+ index: match.index,
72
+ });
73
+ }
74
+ }
75
+ return matches;
76
+ }
77
+ /**
78
+ * Scan a single file's added lines for secrets
79
+ */
80
+ function scanLines(filename, addedLines, config, patterns) {
81
+ const findings = [];
82
+ const seenSecrets = new Set();
83
+ for (const { line, lineNumber } of addedLines) {
84
+ // Pattern-based detection
85
+ const patternMatches = scanWithPatterns(line, patterns, config.allowlist);
86
+ for (const { pattern, match } of patternMatches) {
87
+ const key = `${filename}:${lineNumber}:${match}`;
88
+ if (seenSecrets.has(key))
89
+ continue;
90
+ seenSecrets.add(key);
91
+ findings.push({
92
+ file: filename,
93
+ line: lineNumber,
94
+ type: pattern.name,
95
+ severity: pattern.severity,
96
+ confidence: 'high',
97
+ snippet: (0, mask_1.maskLine)(line, match),
98
+ rawValue: match,
99
+ });
100
+ }
101
+ // Entropy-based detection
102
+ if (config.entropy.enabled) {
103
+ const entropyMatches = (0, patterns_1.detectHighEntropyStrings)(line, config.entropy);
104
+ for (const { value, entropy } of entropyMatches) {
105
+ const key = `${filename}:${lineNumber}:${value}`;
106
+ if (seenSecrets.has(key))
107
+ continue;
108
+ if ((0, config_1.isAllowlisted)(value, config.allowlist))
109
+ continue;
110
+ seenSecrets.add(key);
111
+ let severity = 'low';
112
+ if (entropy >= 5.0) {
113
+ severity = 'high';
114
+ }
115
+ else if (entropy >= 4.5) {
116
+ severity = 'medium';
117
+ }
118
+ findings.push({
119
+ file: filename,
120
+ line: lineNumber,
121
+ type: 'High Entropy String',
122
+ severity,
123
+ confidence: entropy >= 5.0 ? 'high' : 'medium',
124
+ snippet: (0, mask_1.maskLine)(line, value),
125
+ rawValue: value,
126
+ });
127
+ }
128
+ }
129
+ }
130
+ return findings;
131
+ }
132
+ /**
133
+ * Generate markdown report from findings
134
+ */
135
+ function generateReport(findings, filesScanned) {
136
+ const lines = [];
137
+ if (findings.length === 0) {
138
+ lines.push('<!-- keysentinel:comment -->');
139
+ lines.push('');
140
+ lines.push('## :white_check_mark: KeySentinel — Secret Scan');
141
+ lines.push('');
142
+ lines.push('**Status:** ✅ No secrets detected');
143
+ lines.push('');
144
+ lines.push(`_Scanned ${filesScanned} file(s)._`);
145
+ return lines.join('\n');
146
+ }
147
+ const highCount = findings.filter(f => f.severity === 'high').length;
148
+ const mediumCount = findings.filter(f => f.severity === 'medium').length;
149
+ const lowCount = findings.filter(f => f.severity === 'low').length;
150
+ // Status indicator
151
+ const statusIcon = highCount > 0 ? ':x:' : ':warning:';
152
+ const statusText = highCount > 0 ? 'Potential secrets detected' : 'Potential secrets detected';
153
+ // Build summary line
154
+ const parts = [];
155
+ if (highCount > 0)
156
+ parts.push(`:red_circle: High: ${highCount}`);
157
+ if (mediumCount > 0)
158
+ parts.push(`:orange_circle: Medium: ${mediumCount}`);
159
+ if (lowCount > 0)
160
+ parts.push(`:yellow_circle: Low: ${lowCount}`);
161
+ lines.push('<!-- keysentinel:comment -->');
162
+ lines.push('');
163
+ lines.push('## :lock: KeySentinel — Secret Scan');
164
+ lines.push('');
165
+ lines.push(`**Status:** ${statusIcon} ${statusText}`);
166
+ lines.push('');
167
+ lines.push(`**Summary:** **${findings.length} finding${findings.length !== 1 ? 's' : ''}** (${parts.join(' • ')}) • Scanned **${filesScanned}** file(s)`);
168
+ lines.push('');
169
+ lines.push('### Findings');
170
+ lines.push('');
171
+ lines.push('| Severity | File | Line | Rule | Confidence | Preview |');
172
+ lines.push('|:---|:---|---:|:---|:---|:---|');
173
+ for (const finding of findings) {
174
+ const severityLabel = finding.severity === 'high'
175
+ ? ':red_circle: High'
176
+ : finding.severity === 'medium'
177
+ ? ':orange_circle: Medium'
178
+ : ':yellow_circle: Low';
179
+ const lineStr = finding.line !== null ? `${finding.line}` : 'N/A';
180
+ const snippet = finding.snippet.replace(/\|/g, '\\|').replace(/\n/g, ' ');
181
+ const ruleName = finding.type.toLowerCase().replace(/\s+/g, '_');
182
+ lines.push(`| ${severityLabel} | \`${finding.file}\` | ${lineStr} | \`${ruleName}\` | ${finding.confidence} | \`${snippet}\` |`);
183
+ }
184
+ lines.push('');
185
+ lines.push('<details>');
186
+ lines.push('<summary><strong>:white_check_mark: What to do next</strong></summary>');
187
+ lines.push('');
188
+ lines.push('1. **Remove the secret from code** (recommended)');
189
+ lines.push(' - Move to environment variables (e.g. `.env`, GitHub Secrets)');
190
+ lines.push('2. **Rotate the key** if it was real and may have leaked.');
191
+ lines.push('3. If this is a **false positive**, allowlist it:');
192
+ lines.push('');
193
+ lines.push('```yaml');
194
+ lines.push('# .keysentinel.yml');
195
+ lines.push('allowlist:');
196
+ lines.push(" - 'FAKE_SECRET_1234567890'");
197
+ lines.push('```');
198
+ lines.push('');
199
+ lines.push('</details>');
200
+ lines.push('');
201
+ return lines.join('\n');
202
+ }
203
+ /**
204
+ * Check if workflow should fail based on severity threshold
205
+ */
206
+ function shouldFail(findings, failOn) {
207
+ if (failOn === 'off')
208
+ return false;
209
+ const severityOrder = {
210
+ low: 1,
211
+ medium: 2,
212
+ high: 3,
213
+ };
214
+ const threshold = severityOrder[failOn];
215
+ return findings.some(f => severityOrder[f.severity] >= threshold);
216
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "keysentinel",
3
+ "version": "0.1.0",
4
+ "description": "GitHub Action that scans PR diffs for secrets and sensitive data",
5
+ "main": "dist/index.js",
6
+ "bin": { "keysentinel": "lib/cli.js" },
7
+ "files": [ "lib", "dist" ],
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "package": "ncc build src/index.ts -o dist --source-map --license licenses.txt",
11
+ "lint": "eslint src/**/*.ts",
12
+ "test": "jest",
13
+ "all": "npm run build && npm run package",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/Vishrut19/KeySentinel.git"
19
+ },
20
+ "keywords": [
21
+ "github-action",
22
+ "security",
23
+ "secrets",
24
+ "scanning",
25
+ "pull-request"
26
+ ],
27
+ "author": "",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@actions/core": "^1.10.1",
31
+ "@actions/github": "^6.0.0",
32
+ "js-yaml": "^4.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/jest": "^30.0.0",
36
+ "@types/js-yaml": "^4.0.9",
37
+ "@types/node": "^20.11.0",
38
+ "@vercel/ncc": "^0.38.1",
39
+ "jest": "^30.2.0",
40
+ "ts-jest": "^29.4.6",
41
+ "typescript": "^5.3.3"
42
+ }
43
+ }