guardrail-security 1.0.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/dist/attack-surface/analyzer.d.ts +50 -0
- package/dist/attack-surface/analyzer.d.ts.map +1 -0
- package/dist/attack-surface/analyzer.js +83 -0
- package/dist/attack-surface/index.d.ts +5 -0
- package/dist/attack-surface/index.d.ts.map +1 -0
- package/dist/attack-surface/index.js +20 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/languages/index.d.ts +21 -0
- package/dist/languages/index.d.ts.map +1 -0
- package/dist/languages/index.js +78 -0
- package/dist/languages/java-analyzer.d.ts +72 -0
- package/dist/languages/java-analyzer.d.ts.map +1 -0
- package/dist/languages/java-analyzer.js +417 -0
- package/dist/languages/python-analyzer.d.ts +70 -0
- package/dist/languages/python-analyzer.d.ts.map +1 -0
- package/dist/languages/python-analyzer.js +425 -0
- package/dist/license/compatibility-matrix.d.ts +28 -0
- package/dist/license/compatibility-matrix.d.ts.map +1 -0
- package/dist/license/compatibility-matrix.js +323 -0
- package/dist/license/engine.d.ts +77 -0
- package/dist/license/engine.d.ts.map +1 -0
- package/dist/license/engine.js +264 -0
- package/dist/license/index.d.ts +6 -0
- package/dist/license/index.d.ts.map +1 -0
- package/dist/license/index.js +21 -0
- package/dist/sbom/generator.d.ts +108 -0
- package/dist/sbom/generator.d.ts.map +1 -0
- package/dist/sbom/generator.js +271 -0
- package/dist/sbom/index.d.ts +5 -0
- package/dist/sbom/index.d.ts.map +1 -0
- package/dist/sbom/index.js +20 -0
- package/dist/secrets/guardian.d.ts +113 -0
- package/dist/secrets/guardian.d.ts.map +1 -0
- package/dist/secrets/guardian.js +334 -0
- package/dist/secrets/index.d.ts +10 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +30 -0
- package/dist/secrets/patterns.d.ts +42 -0
- package/dist/secrets/patterns.d.ts.map +1 -0
- package/dist/secrets/patterns.js +165 -0
- package/dist/secrets/pre-commit.d.ts +39 -0
- package/dist/secrets/pre-commit.d.ts.map +1 -0
- package/dist/secrets/pre-commit.js +127 -0
- package/dist/secrets/vault-integration.d.ts +83 -0
- package/dist/secrets/vault-integration.d.ts.map +1 -0
- package/dist/secrets/vault-integration.js +295 -0
- package/dist/secrets/vault-providers.d.ts +110 -0
- package/dist/secrets/vault-providers.d.ts.map +1 -0
- package/dist/secrets/vault-providers.js +417 -0
- package/dist/supply-chain/detector.d.ts +80 -0
- package/dist/supply-chain/detector.d.ts.map +1 -0
- package/dist/supply-chain/detector.js +168 -0
- package/dist/supply-chain/index.d.ts +11 -0
- package/dist/supply-chain/index.d.ts.map +1 -0
- package/dist/supply-chain/index.js +26 -0
- package/dist/supply-chain/malicious-db.d.ts +41 -0
- package/dist/supply-chain/malicious-db.d.ts.map +1 -0
- package/dist/supply-chain/malicious-db.js +82 -0
- package/dist/supply-chain/script-analyzer.d.ts +54 -0
- package/dist/supply-chain/script-analyzer.d.ts.map +1 -0
- package/dist/supply-chain/script-analyzer.js +160 -0
- package/dist/supply-chain/typosquat.d.ts +58 -0
- package/dist/supply-chain/typosquat.d.ts.map +1 -0
- package/dist/supply-chain/typosquat.js +257 -0
- package/dist/supply-chain/vulnerability-db.d.ts +114 -0
- package/dist/supply-chain/vulnerability-db.d.ts.map +1 -0
- package/dist/supply-chain/vulnerability-db.js +310 -0
- package/package.json +34 -0
- package/src/__tests__/license/engine.test.ts +250 -0
- package/src/__tests__/supply-chain/typosquat.test.ts +191 -0
- package/src/attack-surface/analyzer.ts +152 -0
- package/src/attack-surface/index.ts +5 -0
- package/src/index.ts +21 -0
- package/src/languages/index.ts +91 -0
- package/src/languages/java-analyzer.ts +490 -0
- package/src/languages/python-analyzer.ts +498 -0
- package/src/license/compatibility-matrix.ts +366 -0
- package/src/license/engine.ts +345 -0
- package/src/license/index.ts +6 -0
- package/src/sbom/generator.ts +355 -0
- package/src/sbom/index.ts +5 -0
- package/src/secrets/guardian.ts +448 -0
- package/src/secrets/index.ts +10 -0
- package/src/secrets/patterns.ts +186 -0
- package/src/secrets/pre-commit.ts +158 -0
- package/src/secrets/vault-integration.ts +360 -0
- package/src/secrets/vault-providers.ts +446 -0
- package/src/supply-chain/detector.ts +252 -0
- package/src/supply-chain/index.ts +11 -0
- package/src/supply-chain/malicious-db.ts +103 -0
- package/src/supply-chain/script-analyzer.ts +194 -0
- package/src/supply-chain/typosquat.ts +302 -0
- package/src/supply-chain/vulnerability-db.ts +386 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preCommitHook = exports.PreCommitHook = void 0;
|
|
4
|
+
const guardian_1 = require("./guardian");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
/**
|
|
9
|
+
* Pre-Commit Hook for Secrets Detection
|
|
10
|
+
*/
|
|
11
|
+
class PreCommitHook {
|
|
12
|
+
/**
|
|
13
|
+
* Generate pre-commit hook script
|
|
14
|
+
*/
|
|
15
|
+
generateHookScript() {
|
|
16
|
+
return `#!/bin/bash
|
|
17
|
+
# Guardrail Secrets Detection Pre-Commit Hook
|
|
18
|
+
|
|
19
|
+
echo "🔍 Scanning for secrets..."
|
|
20
|
+
|
|
21
|
+
# Run secrets scan via API or CLI
|
|
22
|
+
npx Guardrail secrets scan-staged
|
|
23
|
+
|
|
24
|
+
if [ $? -ne 0 ]; then
|
|
25
|
+
echo "❌ Secrets detected! Commit blocked."
|
|
26
|
+
echo "Please remove secrets or add to .gitignore"
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
echo "✅ No secrets detected"
|
|
31
|
+
exit 0
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Scan staged files
|
|
36
|
+
*/
|
|
37
|
+
async scanStagedFiles(gitRoot) {
|
|
38
|
+
try {
|
|
39
|
+
// Get staged files
|
|
40
|
+
const stagedFiles = this.getStagedFiles(gitRoot);
|
|
41
|
+
if (stagedFiles.length === 0) {
|
|
42
|
+
return {
|
|
43
|
+
passed: true,
|
|
44
|
+
detections: 0,
|
|
45
|
+
files: [],
|
|
46
|
+
message: 'No files to scan',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Scan each staged file
|
|
50
|
+
const allDetections = [];
|
|
51
|
+
for (const file of stagedFiles) {
|
|
52
|
+
const detections = await guardian_1.secretsGuardian.scanContent(file.content, file.path, {
|
|
53
|
+
excludeTests: true,
|
|
54
|
+
minConfidence: 0.7,
|
|
55
|
+
});
|
|
56
|
+
if (detections.length > 0) {
|
|
57
|
+
allDetections.push(file.path);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const passed = allDetections.length === 0;
|
|
61
|
+
return {
|
|
62
|
+
passed,
|
|
63
|
+
detections: allDetections.length,
|
|
64
|
+
files: allDetections,
|
|
65
|
+
message: passed
|
|
66
|
+
? 'No secrets detected in staged files'
|
|
67
|
+
: `Secrets detected in ${allDetections.length} file(s)`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw new Error(`Pre-commit scan failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get staged files from git
|
|
76
|
+
*/
|
|
77
|
+
getStagedFiles(gitRoot) {
|
|
78
|
+
try {
|
|
79
|
+
// Get list of staged files
|
|
80
|
+
const output = (0, child_process_1.execSync)('git diff --cached --name-only --diff-filter=ACM', {
|
|
81
|
+
cwd: gitRoot,
|
|
82
|
+
encoding: 'utf-8',
|
|
83
|
+
});
|
|
84
|
+
const filePaths = output.trim().split('\n').filter((f) => f);
|
|
85
|
+
// Read content of each staged file
|
|
86
|
+
const stagedFiles = [];
|
|
87
|
+
for (const filePath of filePaths) {
|
|
88
|
+
try {
|
|
89
|
+
// Get staged content (not working directory)
|
|
90
|
+
const content = (0, child_process_1.execSync)(`git show :${filePath}`, {
|
|
91
|
+
cwd: gitRoot,
|
|
92
|
+
encoding: 'utf-8',
|
|
93
|
+
});
|
|
94
|
+
stagedFiles.push({
|
|
95
|
+
path: filePath,
|
|
96
|
+
content,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
// File might be deleted or binary, skip
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return stagedFiles;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw new Error(`Failed to get staged files: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Install pre-commit hook
|
|
112
|
+
*/
|
|
113
|
+
installHook(gitRoot) {
|
|
114
|
+
const hookPath = (0, path_1.join)(gitRoot, '.git', 'hooks', 'pre-commit');
|
|
115
|
+
const hookScript = this.generateHookScript();
|
|
116
|
+
try {
|
|
117
|
+
(0, fs_1.writeFileSync)(hookPath, hookScript, { mode: 0o755 });
|
|
118
|
+
console.log('✅ Pre-commit hook installed successfully');
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw new Error(`Failed to install hook: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.PreCommitHook = PreCommitHook;
|
|
126
|
+
// Export singleton
|
|
127
|
+
exports.preCommitHook = new PreCommitHook();
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { SecretDetection } from './guardian';
|
|
2
|
+
/**
|
|
3
|
+
* Vault configuration
|
|
4
|
+
*/
|
|
5
|
+
export interface VaultConfig {
|
|
6
|
+
type: 'aws_secrets_manager' | 'hashicorp_vault' | 'azure_keyvault' | 'gcp_secret_manager';
|
|
7
|
+
endpoint?: string;
|
|
8
|
+
region?: string;
|
|
9
|
+
credentials?: {
|
|
10
|
+
accessKeyId?: string;
|
|
11
|
+
secretAccessKey?: string;
|
|
12
|
+
token?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Migration result
|
|
17
|
+
*/
|
|
18
|
+
export interface VaultMigrationResult {
|
|
19
|
+
secretId: string;
|
|
20
|
+
vaultId: string;
|
|
21
|
+
envVarName: string;
|
|
22
|
+
migrated: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Vault Integration for Secrets
|
|
27
|
+
*
|
|
28
|
+
* Helps migrate hardcoded secrets to secure vaults.
|
|
29
|
+
* Supports AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, and GCP Secret Manager.
|
|
30
|
+
*/
|
|
31
|
+
export declare class VaultIntegration {
|
|
32
|
+
private providerCache;
|
|
33
|
+
/**
|
|
34
|
+
* Get or create a vault provider
|
|
35
|
+
*/
|
|
36
|
+
private getProvider;
|
|
37
|
+
/**
|
|
38
|
+
* Test vault connection
|
|
39
|
+
*/
|
|
40
|
+
testConnection(vaultConfig: VaultConfig): Promise<{
|
|
41
|
+
connected: boolean;
|
|
42
|
+
error?: string;
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Migrate secrets to vault
|
|
46
|
+
* Now uses real vault providers instead of simulation
|
|
47
|
+
*/
|
|
48
|
+
migrateToVault(detections: SecretDetection[], vaultConfig: VaultConfig): Promise<VaultMigrationResult[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Retrieve a secret from vault
|
|
51
|
+
*/
|
|
52
|
+
getSecret(vaultConfig: VaultConfig, secretName: string): Promise<string | null>;
|
|
53
|
+
/**
|
|
54
|
+
* List all secrets in vault
|
|
55
|
+
*/
|
|
56
|
+
listSecrets(vaultConfig: VaultConfig): Promise<string[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Delete a secret from vault
|
|
59
|
+
*/
|
|
60
|
+
deleteSecret(vaultConfig: VaultConfig, secretName: string): Promise<boolean>;
|
|
61
|
+
/**
|
|
62
|
+
* Generate environment variable name
|
|
63
|
+
*/
|
|
64
|
+
generateEnvVarName(detection: SecretDetection): string;
|
|
65
|
+
/**
|
|
66
|
+
* Generate code snippet for accessing secret from vault
|
|
67
|
+
*/
|
|
68
|
+
generateCodeSnippet(vaultConfig: VaultConfig, envVarName: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* Generate migration guide
|
|
71
|
+
*/
|
|
72
|
+
generateMigrationGuide(results: VaultMigrationResult[]): string;
|
|
73
|
+
/**
|
|
74
|
+
* Verify a secret exists in vault
|
|
75
|
+
*/
|
|
76
|
+
verifySecret(vaultConfig: VaultConfig, secretName: string): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* Batch migrate with progress callback
|
|
79
|
+
*/
|
|
80
|
+
migrateWithProgress(detections: SecretDetection[], vaultConfig: VaultConfig, onProgress?: (completed: number, total: number, current: string) => void): Promise<VaultMigrationResult[]>;
|
|
81
|
+
}
|
|
82
|
+
export declare const vaultIntegration: VaultIntegration;
|
|
83
|
+
//# sourceMappingURL=vault-integration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault-integration.d.ts","sourceRoot":"","sources":["../../src/secrets/vault-integration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAI7C;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,qBAAqB,GAAG,iBAAiB,GAAG,gBAAgB,GAAG,oBAAoB,CAAC;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE;QACZ,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,aAAa,CAAyC;IAE9D;;OAEG;IACH,OAAO,CAAC,WAAW;IAuBnB;;OAEG;IACG,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAY/F;;;OAGG;IACG,cAAc,CAClB,UAAU,EAAE,eAAe,EAAE,EAC7B,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC,oBAAoB,EAAE,CAAC;IA2DlC;;OAEG;IACG,SAAS,CAAC,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAKrF;;OAEG;IACG,WAAW,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAK9D;;OAEG;IACG,YAAY,CAAC,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKlF;;OAEG;IACH,kBAAkB,CAAC,SAAS,EAAE,eAAe,GAAG,MAAM;IA4BtD;;OAEG;IACH,mBAAmB,CAAC,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IAyDzE;;OAEG;IACH,sBAAsB,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM;IA0B/D;;OAEG;IACG,YAAY,CAAC,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKlF;;OAEG;IACG,mBAAmB,CACvB,UAAU,EAAE,eAAe,EAAE,EAC7B,WAAW,EAAE,WAAW,EACxB,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GACvE,OAAO,CAAC,oBAAoB,EAAE,CAAC;CAoDnC;AAGD,eAAO,MAAM,gBAAgB,kBAAyB,CAAC"}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.vaultIntegration = exports.VaultIntegration = void 0;
|
|
4
|
+
const patterns_1 = require("./patterns");
|
|
5
|
+
const vault_providers_1 = require("./vault-providers");
|
|
6
|
+
/**
|
|
7
|
+
* Vault Integration for Secrets
|
|
8
|
+
*
|
|
9
|
+
* Helps migrate hardcoded secrets to secure vaults.
|
|
10
|
+
* Supports AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, and GCP Secret Manager.
|
|
11
|
+
*/
|
|
12
|
+
class VaultIntegration {
|
|
13
|
+
providerCache = new Map();
|
|
14
|
+
/**
|
|
15
|
+
* Get or create a vault provider
|
|
16
|
+
*/
|
|
17
|
+
getProvider(vaultConfig) {
|
|
18
|
+
const cacheKey = `${vaultConfig.type}_${vaultConfig.endpoint || 'default'}`;
|
|
19
|
+
if (!this.providerCache.has(cacheKey)) {
|
|
20
|
+
const providerConfig = {
|
|
21
|
+
type: vaultConfig.type,
|
|
22
|
+
region: vaultConfig.region,
|
|
23
|
+
endpoint: vaultConfig.endpoint,
|
|
24
|
+
credentials: vaultConfig.credentials,
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
const provider = (0, vault_providers_1.createVaultProvider)(providerConfig);
|
|
28
|
+
this.providerCache.set(cacheKey, provider);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.warn(`Failed to create vault provider, falling back to local: ${error}`);
|
|
32
|
+
this.providerCache.set(cacheKey, new vault_providers_1.LocalEnvProvider());
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return this.providerCache.get(cacheKey);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Test vault connection
|
|
39
|
+
*/
|
|
40
|
+
async testConnection(vaultConfig) {
|
|
41
|
+
try {
|
|
42
|
+
const provider = this.getProvider(vaultConfig);
|
|
43
|
+
const connected = await provider.testConnection();
|
|
44
|
+
return { connected };
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return {
|
|
48
|
+
connected: false,
|
|
49
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Migrate secrets to vault
|
|
55
|
+
* Now uses real vault providers instead of simulation
|
|
56
|
+
*/
|
|
57
|
+
async migrateToVault(detections, vaultConfig) {
|
|
58
|
+
const results = [];
|
|
59
|
+
const provider = this.getProvider(vaultConfig);
|
|
60
|
+
// Test connection first
|
|
61
|
+
const connectionTest = await this.testConnection(vaultConfig);
|
|
62
|
+
if (!connectionTest.connected) {
|
|
63
|
+
return detections.map(detection => ({
|
|
64
|
+
secretId: detection.id || '',
|
|
65
|
+
vaultId: '',
|
|
66
|
+
envVarName: this.generateEnvVarName(detection),
|
|
67
|
+
migrated: false,
|
|
68
|
+
error: `Vault connection failed: ${connectionTest.error}`,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
for (const detection of detections) {
|
|
72
|
+
try {
|
|
73
|
+
const envVarName = this.generateEnvVarName(detection);
|
|
74
|
+
// Extract the actual secret value from detection
|
|
75
|
+
// Note: For security, the actual value should be extracted during scan
|
|
76
|
+
// maskedValue contains the masked version, snippet contains context
|
|
77
|
+
const secretValue = detection.rawValue || detection.location.snippet || '';
|
|
78
|
+
if (!secretValue) {
|
|
79
|
+
results.push({
|
|
80
|
+
secretId: detection.id || '',
|
|
81
|
+
vaultId: '',
|
|
82
|
+
envVarName,
|
|
83
|
+
migrated: false,
|
|
84
|
+
error: 'No secret value found in detection',
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Upload to real vault
|
|
89
|
+
const vaultId = await provider.createSecret(envVarName, secretValue);
|
|
90
|
+
results.push({
|
|
91
|
+
secretId: detection.id || '',
|
|
92
|
+
vaultId,
|
|
93
|
+
envVarName,
|
|
94
|
+
migrated: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
results.push({
|
|
99
|
+
secretId: detection.id || '',
|
|
100
|
+
vaultId: '',
|
|
101
|
+
envVarName: this.generateEnvVarName(detection),
|
|
102
|
+
migrated: false,
|
|
103
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return results;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Retrieve a secret from vault
|
|
111
|
+
*/
|
|
112
|
+
async getSecret(vaultConfig, secretName) {
|
|
113
|
+
const provider = this.getProvider(vaultConfig);
|
|
114
|
+
return provider.getSecret(secretName);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* List all secrets in vault
|
|
118
|
+
*/
|
|
119
|
+
async listSecrets(vaultConfig) {
|
|
120
|
+
const provider = this.getProvider(vaultConfig);
|
|
121
|
+
return provider.listSecrets();
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Delete a secret from vault
|
|
125
|
+
*/
|
|
126
|
+
async deleteSecret(vaultConfig, secretName) {
|
|
127
|
+
const provider = this.getProvider(vaultConfig);
|
|
128
|
+
return provider.deleteSecret(secretName);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Generate environment variable name
|
|
132
|
+
*/
|
|
133
|
+
generateEnvVarName(detection) {
|
|
134
|
+
const typeMap = {
|
|
135
|
+
[patterns_1.SecretType.AWS_ACCESS_KEY]: 'AWS_ACCESS_KEY_ID',
|
|
136
|
+
[patterns_1.SecretType.AWS_SECRET_KEY]: 'AWS_SECRET_ACCESS_KEY',
|
|
137
|
+
[patterns_1.SecretType.GITHUB_TOKEN]: 'GITHUB_TOKEN',
|
|
138
|
+
[patterns_1.SecretType.GOOGLE_API_KEY]: 'GOOGLE_API_KEY',
|
|
139
|
+
[patterns_1.SecretType.STRIPE_KEY]: 'STRIPE_SECRET_KEY',
|
|
140
|
+
[patterns_1.SecretType.JWT_TOKEN]: 'JWT_SECRET',
|
|
141
|
+
[patterns_1.SecretType.PRIVATE_KEY]: 'PRIVATE_KEY',
|
|
142
|
+
[patterns_1.SecretType.DATABASE_URL]: 'DATABASE_URL',
|
|
143
|
+
[patterns_1.SecretType.SLACK_TOKEN]: 'SLACK_TOKEN',
|
|
144
|
+
[patterns_1.SecretType.API_KEY_GENERIC]: 'API_KEY',
|
|
145
|
+
[patterns_1.SecretType.API_KEY]: 'API_KEY',
|
|
146
|
+
[patterns_1.SecretType.TOKEN]: 'TOKEN',
|
|
147
|
+
[patterns_1.SecretType.CERTIFICATE]: 'CERTIFICATE',
|
|
148
|
+
[patterns_1.SecretType.JWT_SECRET]: 'JWT_SECRET',
|
|
149
|
+
[patterns_1.SecretType.PASSWORD]: 'PASSWORD',
|
|
150
|
+
[patterns_1.SecretType.OTHER]: 'SECRET'
|
|
151
|
+
};
|
|
152
|
+
const baseName = typeMap[detection.secretType] || 'SECRET';
|
|
153
|
+
// Add file-based suffix if multiple of same type
|
|
154
|
+
const fileName = detection.filePath.split('/').pop()?.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
|
|
155
|
+
return `${baseName}_${fileName}`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Generate code snippet for accessing secret from vault
|
|
159
|
+
*/
|
|
160
|
+
generateCodeSnippet(vaultConfig, envVarName) {
|
|
161
|
+
switch (vaultConfig.type) {
|
|
162
|
+
case 'aws_secrets_manager':
|
|
163
|
+
return `
|
|
164
|
+
// AWS Secrets Manager
|
|
165
|
+
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
|
|
166
|
+
|
|
167
|
+
const client = new SecretsManagerClient({ region: "${vaultConfig.region || 'us-east-1'}" });
|
|
168
|
+
const response = await client.send(
|
|
169
|
+
new GetSecretValueCommand({ SecretId: "${envVarName}" })
|
|
170
|
+
);
|
|
171
|
+
const secret = response.SecretString;
|
|
172
|
+
`;
|
|
173
|
+
case 'hashicorp_vault':
|
|
174
|
+
return `
|
|
175
|
+
// HashiCorp Vault
|
|
176
|
+
import vault from "node-vault";
|
|
177
|
+
|
|
178
|
+
const vaultClient = vault({
|
|
179
|
+
endpoint: "${vaultConfig.endpoint || 'http://127.0.0.1:8200'}",
|
|
180
|
+
token: process.env.VAULT_TOKEN
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const { data } = await vaultClient.read("secret/data/${envVarName}");
|
|
184
|
+
const secret = data.data.value;
|
|
185
|
+
`;
|
|
186
|
+
case 'azure_keyvault':
|
|
187
|
+
return `
|
|
188
|
+
// Azure Key Vault
|
|
189
|
+
import { SecretClient } from "@azure/keyvault-secrets";
|
|
190
|
+
import { DefaultAzureCredential } from "@azure/identity";
|
|
191
|
+
|
|
192
|
+
const credential = new DefaultAzureCredential();
|
|
193
|
+
const client = new SecretClient("${vaultConfig.endpoint}", credential);
|
|
194
|
+
const secret = await client.getSecret("${envVarName}");
|
|
195
|
+
const value = secret.value;
|
|
196
|
+
`;
|
|
197
|
+
case 'gcp_secret_manager':
|
|
198
|
+
return `
|
|
199
|
+
// GCP Secret Manager
|
|
200
|
+
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
|
|
201
|
+
|
|
202
|
+
const client = new SecretManagerServiceClient();
|
|
203
|
+
const [version] = await client.accessSecretVersion({
|
|
204
|
+
name: 'projects/PROJECT_ID/secrets/${envVarName}/versions/latest',
|
|
205
|
+
});
|
|
206
|
+
const secret = version.payload?.data?.toString();
|
|
207
|
+
`;
|
|
208
|
+
default:
|
|
209
|
+
return `// Use environment variable: process.env.${envVarName}`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Generate migration guide
|
|
214
|
+
*/
|
|
215
|
+
generateMigrationGuide(results) {
|
|
216
|
+
let guide = '# Secrets Migration Guide\n\n';
|
|
217
|
+
guide += '## Detected Secrets\n\n';
|
|
218
|
+
for (const result of results) {
|
|
219
|
+
guide += `### ${result.envVarName}\n`;
|
|
220
|
+
guide += `- Vault ID: ${result.vaultId}\n`;
|
|
221
|
+
guide += `- Status: ${result.migrated ? '✅ Migrated' : '❌ Failed'}\n`;
|
|
222
|
+
if (result.error) {
|
|
223
|
+
guide += `- Error: ${result.error}\n`;
|
|
224
|
+
}
|
|
225
|
+
guide += '\n';
|
|
226
|
+
}
|
|
227
|
+
guide += '## Next Steps\n\n';
|
|
228
|
+
guide += '1. Update your code to fetch secrets from vault\n';
|
|
229
|
+
guide += '2. Remove hardcoded secrets from source code\n';
|
|
230
|
+
guide += '3. Update CI/CD pipelines to use vault credentials\n';
|
|
231
|
+
guide += '4. Test the integration\n';
|
|
232
|
+
guide += '5. Commit and push changes\n';
|
|
233
|
+
return guide;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Verify a secret exists in vault
|
|
237
|
+
*/
|
|
238
|
+
async verifySecret(vaultConfig, secretName) {
|
|
239
|
+
const secret = await this.getSecret(vaultConfig, secretName);
|
|
240
|
+
return secret !== null;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Batch migrate with progress callback
|
|
244
|
+
*/
|
|
245
|
+
async migrateWithProgress(detections, vaultConfig, onProgress) {
|
|
246
|
+
const results = [];
|
|
247
|
+
const provider = this.getProvider(vaultConfig);
|
|
248
|
+
const total = detections.length;
|
|
249
|
+
for (let i = 0; i < detections.length; i++) {
|
|
250
|
+
const detection = detections[i];
|
|
251
|
+
if (!detection)
|
|
252
|
+
continue;
|
|
253
|
+
const envVarName = this.generateEnvVarName(detection);
|
|
254
|
+
if (onProgress) {
|
|
255
|
+
onProgress(i, total, envVarName);
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const secretValue = detection.rawValue || detection.location.snippet || '';
|
|
259
|
+
if (!secretValue) {
|
|
260
|
+
results.push({
|
|
261
|
+
secretId: detection.id || '',
|
|
262
|
+
vaultId: '',
|
|
263
|
+
envVarName,
|
|
264
|
+
migrated: false,
|
|
265
|
+
error: 'No secret value found',
|
|
266
|
+
});
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const vaultId = await provider.createSecret(envVarName, secretValue);
|
|
270
|
+
results.push({
|
|
271
|
+
secretId: detection.id || '',
|
|
272
|
+
vaultId,
|
|
273
|
+
envVarName,
|
|
274
|
+
migrated: true,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
results.push({
|
|
279
|
+
secretId: detection.id || '',
|
|
280
|
+
vaultId: '',
|
|
281
|
+
envVarName,
|
|
282
|
+
migrated: false,
|
|
283
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (onProgress) {
|
|
288
|
+
onProgress(total, total, 'Complete');
|
|
289
|
+
}
|
|
290
|
+
return results;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
exports.VaultIntegration = VaultIntegration;
|
|
294
|
+
// Export singleton
|
|
295
|
+
exports.vaultIntegration = new VaultIntegration();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault Providers
|
|
3
|
+
*
|
|
4
|
+
* Real implementations for secret vault integrations:
|
|
5
|
+
* - AWS Secrets Manager
|
|
6
|
+
* - HashiCorp Vault
|
|
7
|
+
* - Azure Key Vault
|
|
8
|
+
* - GCP Secret Manager
|
|
9
|
+
*/
|
|
10
|
+
export interface VaultProvider {
|
|
11
|
+
name: string;
|
|
12
|
+
createSecret(name: string, value: string): Promise<string>;
|
|
13
|
+
getSecret(name: string): Promise<string | null>;
|
|
14
|
+
deleteSecret(name: string): Promise<boolean>;
|
|
15
|
+
listSecrets(): Promise<string[]>;
|
|
16
|
+
testConnection(): Promise<boolean>;
|
|
17
|
+
}
|
|
18
|
+
export interface VaultProviderConfig {
|
|
19
|
+
type: 'aws_secrets_manager' | 'hashicorp_vault' | 'azure_keyvault' | 'gcp_secret_manager';
|
|
20
|
+
region?: string;
|
|
21
|
+
endpoint?: string;
|
|
22
|
+
projectId?: string;
|
|
23
|
+
credentials?: {
|
|
24
|
+
accessKeyId?: string;
|
|
25
|
+
secretAccessKey?: string;
|
|
26
|
+
token?: string;
|
|
27
|
+
clientId?: string;
|
|
28
|
+
clientSecret?: string;
|
|
29
|
+
tenantId?: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* AWS Secrets Manager Provider
|
|
34
|
+
*/
|
|
35
|
+
export declare class AWSSecretsManagerProvider implements VaultProvider {
|
|
36
|
+
name: string;
|
|
37
|
+
private client;
|
|
38
|
+
private region;
|
|
39
|
+
constructor(config: VaultProviderConfig);
|
|
40
|
+
private getClient;
|
|
41
|
+
createSecret(name: string, value: string): Promise<string>;
|
|
42
|
+
getSecret(name: string): Promise<string | null>;
|
|
43
|
+
deleteSecret(name: string): Promise<boolean>;
|
|
44
|
+
listSecrets(): Promise<string[]>;
|
|
45
|
+
testConnection(): Promise<boolean>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* HashiCorp Vault Provider
|
|
49
|
+
*/
|
|
50
|
+
export declare class HashiCorpVaultProvider implements VaultProvider {
|
|
51
|
+
name: string;
|
|
52
|
+
private client;
|
|
53
|
+
private endpoint;
|
|
54
|
+
private token;
|
|
55
|
+
private mountPath;
|
|
56
|
+
constructor(config: VaultProviderConfig);
|
|
57
|
+
private getClient;
|
|
58
|
+
createSecret(name: string, value: string): Promise<string>;
|
|
59
|
+
getSecret(name: string): Promise<string | null>;
|
|
60
|
+
deleteSecret(name: string): Promise<boolean>;
|
|
61
|
+
listSecrets(): Promise<string[]>;
|
|
62
|
+
testConnection(): Promise<boolean>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Azure Key Vault Provider
|
|
66
|
+
*/
|
|
67
|
+
export declare class AzureKeyVaultProvider implements VaultProvider {
|
|
68
|
+
name: string;
|
|
69
|
+
private client;
|
|
70
|
+
private vaultUrl;
|
|
71
|
+
constructor(config: VaultProviderConfig);
|
|
72
|
+
private getClient;
|
|
73
|
+
createSecret(name: string, value: string): Promise<string>;
|
|
74
|
+
getSecret(name: string): Promise<string | null>;
|
|
75
|
+
deleteSecret(name: string): Promise<boolean>;
|
|
76
|
+
listSecrets(): Promise<string[]>;
|
|
77
|
+
testConnection(): Promise<boolean>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* GCP Secret Manager Provider
|
|
81
|
+
*/
|
|
82
|
+
export declare class GCPSecretManagerProvider implements VaultProvider {
|
|
83
|
+
name: string;
|
|
84
|
+
private client;
|
|
85
|
+
private projectId;
|
|
86
|
+
constructor(config: VaultProviderConfig);
|
|
87
|
+
private getClient;
|
|
88
|
+
createSecret(name: string, value: string): Promise<string>;
|
|
89
|
+
getSecret(name: string): Promise<string | null>;
|
|
90
|
+
deleteSecret(name: string): Promise<boolean>;
|
|
91
|
+
listSecrets(): Promise<string[]>;
|
|
92
|
+
testConnection(): Promise<boolean>;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Factory function to create vault provider
|
|
96
|
+
*/
|
|
97
|
+
export declare function createVaultProvider(config: VaultProviderConfig): VaultProvider;
|
|
98
|
+
/**
|
|
99
|
+
* Local environment provider (for development/testing)
|
|
100
|
+
*/
|
|
101
|
+
export declare class LocalEnvProvider implements VaultProvider {
|
|
102
|
+
name: string;
|
|
103
|
+
private secrets;
|
|
104
|
+
createSecret(name: string, value: string): Promise<string>;
|
|
105
|
+
getSecret(name: string): Promise<string | null>;
|
|
106
|
+
deleteSecret(name: string): Promise<boolean>;
|
|
107
|
+
listSecrets(): Promise<string[]>;
|
|
108
|
+
testConnection(): Promise<boolean>;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=vault-providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault-providers.d.ts","sourceRoot":"","sources":["../../src/secrets/vault-providers.ts"],"names":[],"mappings":"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"}
|