Skip to content

Commit d0a7f1e

Browse files
Merge pull request #70 from Onboardbase/redaction
Redaction
2 parents 172d845 + d459191 commit d0a7f1e

File tree

11 files changed

+577
-6
lines changed

11 files changed

+577
-6
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
node_modules
22
dist
3-
.idea
3+
.idea
4+
yarn.lock
5+
package-lock.json

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,35 @@ git push --force --all
345345
git push --force --tags
346346
```
347347

348+
## How to Initialize SLS Scan as an SDK
349+
350+
```typescript
351+
// Detector Config Interface
352+
export interface DetectorConfig {
353+
regex: string | Record<string, string>;
354+
keywords: string[];
355+
detectorType: string;
356+
group?: string[];
357+
}
358+
359+
import { redactSensitiveData } from "securelog-scan/dist/shared";
360+
361+
const secretRedactionResult = redactSensitiveData("Your API KEY here", {
362+
rawValue: "String you want to check for secrets here",
363+
maskedValue: "*", // that is what detected secrets should be replaced with
364+
visibleChars: 3, // how many characters should be visible among detected secrets
365+
366+
// An Array of DetectorConfig, example below
367+
customDetectors: [
368+
{
369+
regex: "\\b(FLWSECK-[0-9a-z]{32}-X)\\b",
370+
detectorType: "Flutterwave",
371+
keywords: ["FLWSECK-"],
372+
},
373+
],
374+
}); // returns {rawValue: "Your returned string with secrets redacted", secrets: ["Array of secrets that was found in string"]}
375+
```
376+
348377
# Contributing
349378

350379
Feel free to contribute to this project by opening issues or submitting pull requests. Contribute to [SECRET DETECTORS](./DETECTORS.md).

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "securelog-scan",
3-
"version": "3.0.18",
3+
"version": "3.0.19",
44
"description": "A CLI tool to scan codebases for potential secrets.",
55
"main": "dist/index.js",
66
"author": {
@@ -30,6 +30,7 @@
3030
"cli-table3": "^0.6.5",
3131
"commander": "^11.0.0",
3232
"google-auth-library": "^9.14.1",
33+
"js-yaml": "^4.1.0",
3334
"mongodb": "^6.8.1",
3435
"mysql2": "^3.11.3",
3536
"pg": "^8.13.0",
@@ -42,6 +43,7 @@
4243
"homepage": "https://thesecurelog.com",
4344
"devDependencies": {
4445
"@types/ahocorasick": "^1.0.0",
46+
"@types/js-yaml": "^4.0.9",
4547
"@types/node": "^22.6.1",
4648
"@types/pg": "^8.11.10",
4749
"eslint": "^9.11.1",

redaction-config.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
patterns:
2+
email:
3+
pattern: '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}'
4+
replacement: '[EMAIL_REDACTED]'
5+
6+
ssn:
7+
pattern: '\d{3}-\d{2}-\d{4}'
8+
replacement: '[SSN_REDACTED]'
9+
10+
creditCard:
11+
pattern: '\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}'
12+
replacement: '[CREDIT_CARD_REDACTED]'
13+
14+
apiKey:
15+
pattern: 'sk_(test|live)_[0-9a-zA-Z]+'
16+
replacement: '[API_KEY_REDACTED]'
17+
18+
jwt:
19+
pattern: 'eyJ[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*'
20+
replacement: '[JWT_REDACTED]'
21+
22+
password:
23+
pattern: 'password:[^@\s]*@'
24+
replacement: 'password:****@'
25+
26+
cache:
27+
size: 1000

src/decay.ts

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import Re2 from 're2';
2+
import { DataFormatHandlers } from './shared';
3+
import { RedactionConfig } from './types';
4+
import { defaultRedactionConfigs } from './shared/default-decay.config';
5+
import yaml from 'js-yaml';
6+
import fs from 'fs';
7+
8+
process.removeAllListeners('warning');
9+
10+
interface YamlConfig {
11+
patterns: RedactionConfig;
12+
cache: {
13+
size: number;
14+
};
15+
}
16+
17+
export class Decay {
18+
private readonly configs: RedactionConfig;
19+
private readonly formatHandlers: DataFormatHandlers;
20+
private readonly compiledPatterns: Map<string, Re2>;
21+
private readonly cache: Map<string, string>;
22+
private readonly cacheSize: number;
23+
24+
/**
25+
* Create a new Decay instance from YAML config file or default config
26+
* @param configPath Optional path to YAML config file
27+
*/
28+
constructor(configPath?: string) {
29+
const config = this.loadConfig(configPath);
30+
this.configs = config.patterns;
31+
this.formatHandlers = new DataFormatHandlers();
32+
this.compiledPatterns = this.compilePatterns();
33+
this.cache = new Map();
34+
this.cacheSize = config.cache?.size || 1000;
35+
}
36+
37+
/**
38+
* Load configuration from YAML file or use defaults
39+
*/
40+
private loadConfig(configPath?: string): YamlConfig {
41+
if (!configPath) {
42+
return {
43+
patterns: defaultRedactionConfigs,
44+
cache: { size: 1000 }
45+
};
46+
}
47+
48+
try {
49+
const fileContents = fs.readFileSync(configPath, 'utf8');
50+
const config = yaml.load(fileContents) as YamlConfig;
51+
52+
// Validate the loaded config
53+
if (!config.patterns || typeof config.patterns !== 'object') {
54+
throw new Error('Invalid config: missing or invalid patterns section');
55+
}
56+
57+
// Validate each pattern
58+
for (const [key, value] of Object.entries(config.patterns)) {
59+
if (!value.pattern || !value.replacement) {
60+
throw new Error(`Invalid config for pattern ${key}: missing pattern or replacement`);
61+
}
62+
63+
// Validate pattern can be compiled
64+
try {
65+
new Re2(value.pattern);
66+
} catch (error: any) {
67+
throw new Error(`Invalid regex pattern for ${key}: ${error.message}`);
68+
}
69+
}
70+
71+
return config;
72+
} catch (error) {
73+
console.error('Error loading config:', error);
74+
console.warn('Falling back to default configuration');
75+
return {
76+
patterns: defaultRedactionConfigs,
77+
cache: { size: 1000 }
78+
};
79+
}
80+
}
81+
/**
82+
* Compiles patterns using RE2 with optimization flags
83+
*/
84+
private compilePatterns(): Map<string, Re2> {
85+
const compiled = new Map();
86+
for (const [key, config] of Object.entries(this.configs)) {
87+
try {
88+
// Use RE2 optimization flags
89+
compiled.set(key, new Re2(config.pattern));
90+
} catch (error) {
91+
console.error(`Failed to compile pattern for ${key}:`, error);
92+
}
93+
}
94+
return compiled;
95+
}
96+
97+
/**
98+
* Generates cache key for input data
99+
*/
100+
private generateCacheKey(data: any): string {
101+
if (typeof data === 'string') {
102+
return data;
103+
}
104+
try {
105+
return JSON.stringify(data);
106+
} catch {
107+
return String(data);
108+
}
109+
}
110+
111+
/**
112+
* Main redaction function with caching
113+
*/
114+
public redact(data: any): any {
115+
const cacheKey = this.generateCacheKey(data);
116+
117+
// Check cache first
118+
const cached = this.cache.get(cacheKey);
119+
if (cached) {
120+
return this.intelligentParse(cached, 'string');
121+
}
122+
123+
try {
124+
const { stringified, format } = this.intelligentStringify(data);
125+
const redacted = this.redactSensitiveData(stringified);
126+
127+
// Cache the result
128+
if (this.cache.size >= this.cacheSize) {
129+
const firstKey = this.cache.keys().next().value;
130+
if(firstKey) this.cache.delete(firstKey);
131+
}
132+
this.cache.set(cacheKey, redacted);
133+
134+
return this.intelligentParse(redacted, format);
135+
} catch (error) {
136+
console.error('Error during redaction:', error);
137+
const fallback = this.redactSensitiveData(String(data));
138+
return fallback;
139+
}
140+
}
141+
142+
/**
143+
* Optimized redaction using compiled RE2 patterns
144+
*/
145+
private redactSensitiveData(text: string): string {
146+
let redactedText = text;
147+
148+
// Sort patterns by length for better matching
149+
const sortedPatterns = Array.from(this.compiledPatterns.entries())
150+
.sort(([, a], [, b]) => b.toString().length - a.toString().length);
151+
152+
for (const [key, pattern] of sortedPatterns) {
153+
const replacement = this.configs[key].replacement;
154+
try {
155+
redactedText = redactedText.replace(pattern, replacement);
156+
} catch (error) {
157+
console.warn(`Pattern ${key} failed:`, error);
158+
}
159+
}
160+
161+
return redactedText;
162+
}
163+
164+
/**
165+
* Enhanced string conversion with format detection
166+
*/
167+
private intelligentStringify(data: any): { stringified: string; format: string } {
168+
if (typeof data === 'string') {
169+
const format = this.formatHandlers.detectFormat(data);
170+
return { stringified: data, format };
171+
}
172+
173+
const jsonString = JSON.stringify(data, null, 2);
174+
return { stringified: jsonString, format: 'json' };
175+
}
176+
177+
/**
178+
* Intelligent parsing based on detected format
179+
*/
180+
private intelligentParse(text: string, format: string): any {
181+
const handler = this.formatHandlers.getHandler(format);
182+
if (handler) {
183+
try {
184+
return handler.parse(text);
185+
} catch (error) {
186+
console.warn(`Failed to parse as ${format}, falling back to string`);
187+
}
188+
}
189+
return text;
190+
}
191+
192+
}
193+
194+
// Performance test suite
195+
function runPerformanceTest(redactor: Decay) {
196+
const testCases = {
197+
simple: "Email: test@example.com",
198+
complex: {
199+
users: Array(1000).fill(null).map((_, i) => ({
200+
id: i,
201+
email: `user${i}@example.com`,
202+
password: `secret${i}`,
203+
ssn: "123-45-6789",
204+
creditCard: "4111-1111-1111-1111",
205+
address: "123 Main St, New York, NY 12345",
206+
}))
207+
},
208+
nested: {
209+
level1: {
210+
level2: {
211+
level3: {
212+
data: Array(100).fill({
213+
apiKey: "sk_test_123456789",
214+
jwt: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
215+
})
216+
}
217+
}
218+
}
219+
}
220+
};
221+
222+
console.time('Initial redaction');
223+
redactor.redact(testCases.complex);
224+
console.timeEnd('Initial redaction');
225+
226+
console.time('Cached redaction');
227+
redactor.redact(testCases.complex);
228+
console.timeEnd('Cached redaction');
229+
230+
console.time('Nested redaction');
231+
redactor.redact(testCases.nested);
232+
console.timeEnd('Nested redaction');
233+
}
234+
235+
export const decay = (config?: string)=> new Decay(config);

src/index.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import { scan } from "./scan";
66
import { analyzers } from "./analyzers";
77
import { removeSecretsFromGitHistory } from "./gitRewrite";
88
import { scanString } from "./scanString";
9-
import { ScanOptions, ScanStringOptions } from "./types";
9+
import { DecayOptions, ScanOptions, ScanStringOptions } from "./types";
10+
import { decay } from "./decay";
11+
import { readInputFile } from "./shared/file-input";
1012

1113
const program = new Command();
1214

@@ -77,4 +79,37 @@ program
7779
.description("Scan secrets in a string")
7880
.action((options: ScanStringOptions) => scanString(options));
7981

82+
program
83+
.command("decay")
84+
.argument("[data]", "Data to decay (optional if using --file)")
85+
.option("--config <string>", "Path to configuration file")
86+
.option("--file <string>", "Path to input file containing data to decay")
87+
.description("Decay sensitive data from input or file")
88+
.action(async (data: string | undefined, options: DecayOptions) => {
89+
try {
90+
const decayer = decay(options.config);
91+
92+
let inputData: any;
93+
if (options.file) {
94+
inputData = readInputFile(options.file);
95+
} else if (data) {
96+
inputData = data;
97+
} else {
98+
throw new Error(
99+
"No input provided. Use --file or provide data directly."
100+
);
101+
}
102+
103+
const redactedData = decayer.redact(inputData);
104+
105+
console.log(
106+
typeof redactedData === "object"
107+
? JSON.stringify(redactedData, null, 2)
108+
: redactedData
109+
);
110+
} catch (error: any) {
111+
console.error("Error:", error.message);
112+
process.exit(1);
113+
}
114+
});
80115
program.parse(process.argv);

0 commit comments

Comments
 (0)