secureflow-cli 1.0.0 → 1.2.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/git.d.ts +51 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +96 -0
- package/dist/git.js.map +1 -0
- package/dist/index.js +91 -40
- package/dist/index.js.map +1 -1
- package/dist/sarif.d.ts +84 -0
- package/dist/sarif.d.ts.map +1 -0
- package/dist/sarif.js +135 -0
- package/dist/sarif.js.map +1 -0
- package/dist/scanner.d.ts +93 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +266 -0
- package/dist/scanner.js.map +1 -0
- package/package.json +35 -32
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git access for the pre-commit hook.
|
|
3
|
+
*
|
|
4
|
+
* The one thing this module exists to get right: a pre-commit hook must check
|
|
5
|
+
* **staged content**, not what happens to be on disk. The previous
|
|
6
|
+
* implementation asked git for the staged *paths* and then read those paths
|
|
7
|
+
* from the working tree, which is a different thing and lets a secret through:
|
|
8
|
+
*
|
|
9
|
+
* echo 'console.log(process.env.AWS_SECRET_ACCESS_KEY);' >> src/debug.ts
|
|
10
|
+
* git add src/debug.ts # the secret is staged
|
|
11
|
+
* sed -i '' '$d' src/debug.ts # tidy the working tree, forget to re-stage
|
|
12
|
+
* git commit -m "wip" # ✅ SecureFlow scan passed.
|
|
13
|
+
*
|
|
14
|
+
* The commit contains the logged secret; the scanner read the clean working
|
|
15
|
+
* tree. "Stage a chunk, keep editing" is the ordinary `git add -p` workflow, so
|
|
16
|
+
* this is not a corner case, and it fails in the direction that lets secrets
|
|
17
|
+
* through (#593).
|
|
18
|
+
*
|
|
19
|
+
* The inverse is just as bad: unstaged debug code blocks a commit that does not
|
|
20
|
+
* contain it, which teaches people to reach for `--no-verify`.
|
|
21
|
+
*/
|
|
22
|
+
/** A problem with the repository or the git invocation, not with the content. */
|
|
23
|
+
export declare class GitError extends Error {
|
|
24
|
+
readonly cause?: unknown;
|
|
25
|
+
constructor(message: string, cause?: unknown);
|
|
26
|
+
}
|
|
27
|
+
/** Whether the current directory is inside a work tree. */
|
|
28
|
+
export declare function isGitRepository(): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Paths staged for commit.
|
|
31
|
+
*
|
|
32
|
+
* `-z` is what makes this correct for real repositories. Without it git honours
|
|
33
|
+
* `core.quotePath`, which defaults to **true**, so a staged `src/café.ts` comes
|
|
34
|
+
* back as the literal 15 characters `"src/caf\303\251.ts"` — quotes and octal
|
|
35
|
+
* escapes included. `path.resolve` then built a path that does not exist,
|
|
36
|
+
* `existsSync` returned false, and the file was skipped with no warning and no
|
|
37
|
+
* non-zero exit. The same applied to any path containing a space or a `"`.
|
|
38
|
+
*
|
|
39
|
+
* `--diff-filter=ACMR` drops deletions, which have no staged content to read,
|
|
40
|
+
* and keeps renames, which do.
|
|
41
|
+
*/
|
|
42
|
+
export declare function getStagedFiles(): string[];
|
|
43
|
+
/**
|
|
44
|
+
* The staged content of `path` — the bytes that will actually be committed.
|
|
45
|
+
*
|
|
46
|
+
* `git show :<path>` reads the index entry, which is the whole point. Returns
|
|
47
|
+
* null when the blob cannot be read (a conflicted entry, a submodule pointer,
|
|
48
|
+
* a symlink), so the caller can report it rather than treat it as clean.
|
|
49
|
+
*/
|
|
50
|
+
export declare function readStagedContent(path: string): string | null;
|
|
51
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAaH,iFAAiF;AACjF,qBAAa,QAAS,SAAQ,KAAK;IACJ,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO;IAArD,YAAY,OAAO,EAAE,MAAM,EAAW,KAAK,CAAC,EAAE,OAAO,EAGpD;CACF;AASD,2DAA2D;AAC3D,wBAAgB,eAAe,IAAI,OAAO,CAMzC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAgBzC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM7D"}
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git access for the pre-commit hook.
|
|
3
|
+
*
|
|
4
|
+
* The one thing this module exists to get right: a pre-commit hook must check
|
|
5
|
+
* **staged content**, not what happens to be on disk. The previous
|
|
6
|
+
* implementation asked git for the staged *paths* and then read those paths
|
|
7
|
+
* from the working tree, which is a different thing and lets a secret through:
|
|
8
|
+
*
|
|
9
|
+
* echo 'console.log(process.env.AWS_SECRET_ACCESS_KEY);' >> src/debug.ts
|
|
10
|
+
* git add src/debug.ts # the secret is staged
|
|
11
|
+
* sed -i '' '$d' src/debug.ts # tidy the working tree, forget to re-stage
|
|
12
|
+
* git commit -m "wip" # ✅ SecureFlow scan passed.
|
|
13
|
+
*
|
|
14
|
+
* The commit contains the logged secret; the scanner read the clean working
|
|
15
|
+
* tree. "Stage a chunk, keep editing" is the ordinary `git add -p` workflow, so
|
|
16
|
+
* this is not a corner case, and it fails in the direction that lets secrets
|
|
17
|
+
* through (#593).
|
|
18
|
+
*
|
|
19
|
+
* The inverse is just as bad: unstaged debug code blocks a commit that does not
|
|
20
|
+
* contain it, which teaches people to reach for `--no-verify`.
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from 'child_process';
|
|
23
|
+
/**
|
|
24
|
+
* Buffer ceiling for git output.
|
|
25
|
+
*
|
|
26
|
+
* `execSync` defaults to 1 MB. A commit touching enough files overflowed it,
|
|
27
|
+
* and the catch block then printed "Are you in a git repository?" — which was
|
|
28
|
+
* not the problem, and blocked the commit for a reason nobody could act on.
|
|
29
|
+
*/
|
|
30
|
+
const MAX_GIT_BUFFER = 64 * 1024 * 1024;
|
|
31
|
+
/** A problem with the repository or the git invocation, not with the content. */
|
|
32
|
+
export class GitError extends Error {
|
|
33
|
+
cause;
|
|
34
|
+
constructor(message, cause) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.cause = cause;
|
|
37
|
+
this.name = 'GitError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function git(args) {
|
|
41
|
+
return execFileSync('git', args, {
|
|
42
|
+
encoding: 'utf-8',
|
|
43
|
+
maxBuffer: MAX_GIT_BUFFER,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Whether the current directory is inside a work tree. */
|
|
47
|
+
export function isGitRepository() {
|
|
48
|
+
try {
|
|
49
|
+
return git(['rev-parse', '--is-inside-work-tree']).trim() === 'true';
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Paths staged for commit.
|
|
57
|
+
*
|
|
58
|
+
* `-z` is what makes this correct for real repositories. Without it git honours
|
|
59
|
+
* `core.quotePath`, which defaults to **true**, so a staged `src/café.ts` comes
|
|
60
|
+
* back as the literal 15 characters `"src/caf\303\251.ts"` — quotes and octal
|
|
61
|
+
* escapes included. `path.resolve` then built a path that does not exist,
|
|
62
|
+
* `existsSync` returned false, and the file was skipped with no warning and no
|
|
63
|
+
* non-zero exit. The same applied to any path containing a space or a `"`.
|
|
64
|
+
*
|
|
65
|
+
* `--diff-filter=ACMR` drops deletions, which have no staged content to read,
|
|
66
|
+
* and keeps renames, which do.
|
|
67
|
+
*/
|
|
68
|
+
export function getStagedFiles() {
|
|
69
|
+
let output;
|
|
70
|
+
try {
|
|
71
|
+
output = git(['diff', '--cached', '--name-only', '-z', '--diff-filter=ACMR']);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new GitError(isGitRepository()
|
|
75
|
+
? 'Could not list staged files. `git diff --cached` failed.'
|
|
76
|
+
: 'Not a git repository (or git is not on PATH).', error);
|
|
77
|
+
}
|
|
78
|
+
// NUL-separated, with a trailing separator on a non-empty list.
|
|
79
|
+
return output.split('\u0000').filter((entry) => entry.length > 0);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The staged content of `path` — the bytes that will actually be committed.
|
|
83
|
+
*
|
|
84
|
+
* `git show :<path>` reads the index entry, which is the whole point. Returns
|
|
85
|
+
* null when the blob cannot be read (a conflicted entry, a submodule pointer,
|
|
86
|
+
* a symlink), so the caller can report it rather than treat it as clean.
|
|
87
|
+
*/
|
|
88
|
+
export function readStagedContent(path) {
|
|
89
|
+
try {
|
|
90
|
+
return git(['show', `:${path}`]);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=git.js.map
|
package/dist/git.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C;;;;;;GAMG;AACH,MAAM,cAAc,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAExC,iFAAiF;AACjF,MAAM,OAAO,QAAS,SAAQ,KAAK;IACK,KAAK;IAA3C,YAAY,OAAe,EAAW,KAAe;QACnD,KAAK,CAAC,OAAO,CAAC,CAAC;qBADqB,KAAK;QAEzC,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AAED,SAAS,GAAG,CAAC,IAAc;IACzB,OAAO,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE;QAC/B,QAAQ,EAAE,OAAO;QACjB,SAAS,EAAE,cAAc;KAC1B,CAAC,CAAC;AACL,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,eAAe;IAC7B,IAAI,CAAC;QACH,OAAO,GAAG,CAAC,CAAC,WAAW,EAAE,uBAAuB,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc;IAC5B,IAAI,MAAc,CAAC;IAEnB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,IAAI,EAAE,oBAAoB,CAAC,CAAC,CAAC;IAChF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,QAAQ,CAChB,eAAe,EAAE;YACf,CAAC,CAAC,0DAA0D;YAC5D,CAAC,CAAC,+CAA+C,EACnD,KAAK,CACN,CAAC;IACJ,CAAC;IAED,gEAAgE;IAChE,OAAO,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACpE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,CAAC;QACH,OAAO,GAAG,CAAC,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,50 +1,101 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
const
|
|
6
|
-
function
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { GitError, getStagedFiles, readStagedContent } from './git.js';
|
|
4
|
+
import { scanFile, formatScanResults } from './scanner.js';
|
|
5
|
+
const VERBOSE = process.argv.includes('--verbose');
|
|
6
|
+
function parseFormatArg() {
|
|
7
|
+
const formatIndex = process.argv.findIndex((arg) => arg === '--format');
|
|
8
|
+
if (formatIndex !== -1) {
|
|
9
|
+
const valStr = process.argv[formatIndex + 1];
|
|
10
|
+
if (valStr) {
|
|
11
|
+
const val = valStr.toLowerCase();
|
|
12
|
+
if (val === 'sarif' || val === 'json' || val === 'text') {
|
|
13
|
+
return val;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return 'text';
|
|
18
|
+
}
|
|
19
|
+
function parseOutputArg() {
|
|
20
|
+
const outIndex = process.argv.findIndex((arg) => arg === '-o' || arg === '--output');
|
|
21
|
+
if (outIndex !== -1) {
|
|
22
|
+
const valStr = process.argv[outIndex + 1];
|
|
23
|
+
if (valStr) {
|
|
24
|
+
return valStr;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function reportSkipped(result) {
|
|
30
|
+
if (VERBOSE && result.skipped) {
|
|
31
|
+
console.log(` ↷ skipped ${result.path} (${result.skipped})`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function reportViolations(result) {
|
|
35
|
+
for (const violation of result.violations) {
|
|
36
|
+
console.error(`🚨 [SecureFlow] Secret logging detected in ${result.path}:${violation.line}`);
|
|
37
|
+
console.error(` -> ${violation.text}`);
|
|
38
|
+
console.error(` why: ${violation.reason} passed to a console call`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function main() {
|
|
42
|
+
const format = parseFormatArg();
|
|
43
|
+
const outputPath = parseOutputArg();
|
|
44
|
+
let staged;
|
|
7
45
|
try {
|
|
8
|
-
|
|
9
|
-
return output.split('\n').filter(Boolean);
|
|
46
|
+
staged = getStagedFiles();
|
|
10
47
|
}
|
|
11
48
|
catch (error) {
|
|
12
|
-
console.error(
|
|
13
|
-
|
|
49
|
+
console.error(`❌ [SecureFlow] ${error instanceof GitError ? error.message : String(error)}`);
|
|
50
|
+
return 1;
|
|
14
51
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
52
|
+
const fileResults = [];
|
|
53
|
+
const unreadable = [];
|
|
54
|
+
let violationCount = 0;
|
|
55
|
+
if (staged.length > 0) {
|
|
56
|
+
for (const path of staged) {
|
|
57
|
+
const content = readStagedContent(path);
|
|
58
|
+
if (content === null) {
|
|
59
|
+
unreadable.push(path);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const result = scanFile(path, content);
|
|
63
|
+
fileResults.push(result);
|
|
64
|
+
if (format === 'text') {
|
|
65
|
+
reportSkipped(result);
|
|
66
|
+
reportViolations(result);
|
|
67
|
+
}
|
|
68
|
+
violationCount += result.violations.length;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (format === 'sarif' || format === 'json') {
|
|
72
|
+
const outputString = formatScanResults(fileResults, format);
|
|
73
|
+
if (outputPath) {
|
|
74
|
+
fs.writeFileSync(outputPath, outputString, 'utf-8');
|
|
75
|
+
console.log(`📄 [SecureFlow] Scan report exported in ${format.toUpperCase()} format to ${outputPath}`);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.log(outputString);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (outputPath) {
|
|
82
|
+
const textOutput = formatScanResults(fileResults, 'text');
|
|
83
|
+
fs.writeFileSync(outputPath, textOutput, 'utf-8');
|
|
84
|
+
console.log(`📄 [SecureFlow] Scan report written to ${outputPath}`);
|
|
85
|
+
}
|
|
86
|
+
if (unreadable.length > 0 && format === 'text') {
|
|
87
|
+
console.warn(`⚠️ [SecureFlow] Could not read ${unreadable.length} staged entr${unreadable.length === 1 ? 'y' : 'ies'} (submodule, symlink or conflicted): ${unreadable.join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
if (violationCount > 0) {
|
|
90
|
+
if (format === 'text') {
|
|
91
|
+
console.error(`\n❌ SecureFlow blocked this commit: ${violationCount} secret-logging violation${violationCount === 1 ? '' : 's'}. Remove the exposed secrets/env variables, then re-stage.`);
|
|
38
92
|
}
|
|
39
|
-
|
|
40
|
-
if (hasViolations) {
|
|
41
|
-
console.error('\n❌ SecureFlow blocked this commit. Please remove the exposed secrets/env variables.');
|
|
42
|
-
process.exit(1);
|
|
93
|
+
return 1;
|
|
43
94
|
}
|
|
44
|
-
|
|
45
|
-
console.log(
|
|
46
|
-
process.exit(0);
|
|
95
|
+
if (format === 'text') {
|
|
96
|
+
console.log(`✅ SecureFlow scan passed (${staged.length} staged file(s)).`);
|
|
47
97
|
}
|
|
98
|
+
return 0;
|
|
48
99
|
}
|
|
49
|
-
|
|
100
|
+
process.exit(main());
|
|
50
101
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AACvE,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAA0C,MAAM,cAAc,CAAC;AAEnG,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAEnD,SAAS,cAAc;IACrB,MAAM,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,CAAC,CAAC;IACxE,IAAI,WAAW,KAAK,CAAC,CAAC,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;QAC7C,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;YACjC,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;gBACxD,OAAO,GAAmB,CAAC;YAC7B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,cAAc;IACrB,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,UAAU,CAAC,CAAC;IACrF,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;QACpB,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;QAC1C,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,aAAa,CAAC,MAAsB;IAC3C,IAAI,OAAO,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,eAAe,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC;IAChE,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAsB;IAC9C,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QAC1C,OAAO,CAAC,KAAK,CAAC,8CAA8C,MAAM,CAAC,IAAI,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7F,OAAO,CAAC,KAAK,CAAC,QAAQ,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;QACxC,OAAO,CAAC,KAAK,CAAC,UAAU,SAAS,CAAC,MAAM,2BAA2B,CAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED,SAAS,IAAI;IACX,MAAM,MAAM,GAAG,cAAc,EAAE,CAAC;IAChC,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;IACpC,IAAI,MAAgB,CAAC;IAErB,IAAI,CAAC;QACH,MAAM,GAAG,cAAc,EAAE,CAAC;IAC5B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,kBAAkB,KAAK,YAAY,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC7F,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,WAAW,GAAqB,EAAE,CAAC;IACzC,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,IAAI,cAAc,GAAG,CAAC,CAAC;IAEvB,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;YAC1B,MAAM,OAAO,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;YAExC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrB,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACtB,SAAS;YACX,CAAC;YAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YACvC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACzB,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;gBACtB,aAAa,CAAC,MAAM,CAAC,CAAC;gBACtB,gBAAgB,CAAC,MAAM,CAAC,CAAC;YAC3B,CAAC;YACD,cAAc,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;QAC7C,CAAC;IACH,CAAC;IAED,IAAI,MAAM,KAAK,OAAO,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QAC5C,MAAM,YAAY,GAAG,iBAAiB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC5D,IAAI,UAAU,EAAE,CAAC;YACf,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;YACpD,OAAO,CAAC,GAAG,CAAC,2CAA2C,MAAM,CAAC,WAAW,EAAE,cAAc,UAAU,EAAE,CAAC,CAAC;QACzG,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;SAAM,IAAI,UAAU,EAAE,CAAC;QACtB,MAAM,UAAU,GAAG,iBAAiB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC1D,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;QAClD,OAAO,CAAC,GAAG,CAAC,0CAA0C,UAAU,EAAE,CAAC,CAAC;IACtE,CAAC;IAED,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QAC/C,OAAO,CAAC,IAAI,CACV,mCAAmC,UAAU,CAAC,MAAM,eAClD,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAClC,wCAAwC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAChE,CAAC;IACJ,CAAC;IAED,IAAI,cAAc,GAAG,CAAC,EAAE,CAAC;QACvB,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACtB,OAAO,CAAC,KAAK,CACX,uCAAuC,cAAc,4BACnD,cAAc,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAC9B,4DAA4D,CAC7D,CAAC;QACJ,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,6BAA6B,MAAM,CAAC,MAAM,mBAAmB,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC"}
|
package/dist/sarif.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SARIF (Static Analysis Results Interchange Format) Exporter for SecureFlow CLI (#728)
|
|
3
|
+
*
|
|
4
|
+
* Converts CLI scan results into standardized OASIS SARIF v2.1.0 schema format
|
|
5
|
+
* for direct ingestion by GitHub Advanced Security, GitLab Security Dashboards, and enterprise CI/CD systems.
|
|
6
|
+
*/
|
|
7
|
+
import type { FileScanResult } from './scanner.js';
|
|
8
|
+
export interface SarifArtifactLocation {
|
|
9
|
+
uri: string;
|
|
10
|
+
uriBaseId?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SarifRegion {
|
|
13
|
+
startLine: number;
|
|
14
|
+
startColumn?: number;
|
|
15
|
+
endLine?: number;
|
|
16
|
+
endColumn?: number;
|
|
17
|
+
snippet?: {
|
|
18
|
+
text: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface SarifPhysicalLocation {
|
|
22
|
+
artifactLocation: SarifArtifactLocation;
|
|
23
|
+
region: SarifRegion;
|
|
24
|
+
}
|
|
25
|
+
export interface SarifLocation {
|
|
26
|
+
physicalLocation: SarifPhysicalLocation;
|
|
27
|
+
}
|
|
28
|
+
export interface SarifReportingDescriptor {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
shortDescription: {
|
|
32
|
+
text: string;
|
|
33
|
+
};
|
|
34
|
+
fullDescription?: {
|
|
35
|
+
text: string;
|
|
36
|
+
};
|
|
37
|
+
defaultConfiguration?: {
|
|
38
|
+
level: 'error' | 'warning' | 'note' | 'none';
|
|
39
|
+
};
|
|
40
|
+
helpUri?: string;
|
|
41
|
+
properties?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
export interface SarifResult {
|
|
44
|
+
ruleId: string;
|
|
45
|
+
ruleIndex?: number;
|
|
46
|
+
level: 'error' | 'warning' | 'note' | 'none';
|
|
47
|
+
message: {
|
|
48
|
+
text: string;
|
|
49
|
+
};
|
|
50
|
+
locations: SarifLocation[];
|
|
51
|
+
properties?: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
export interface SarifRun {
|
|
54
|
+
tool: {
|
|
55
|
+
driver: {
|
|
56
|
+
name: string;
|
|
57
|
+
organization?: string;
|
|
58
|
+
version?: string;
|
|
59
|
+
semanticVersion?: string;
|
|
60
|
+
informationUri?: string;
|
|
61
|
+
rules: SarifReportingDescriptor[];
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
results: SarifResult[];
|
|
65
|
+
}
|
|
66
|
+
export interface SarifDocument {
|
|
67
|
+
$schema: string;
|
|
68
|
+
version: '2.1.0';
|
|
69
|
+
runs: SarifRun[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Maps CLI scan results into a valid OASIS SARIF v2.1.0 document object.
|
|
73
|
+
*/
|
|
74
|
+
export declare function generateSarifReport(scanResults: FileScanResult[], options?: {
|
|
75
|
+
toolVersion?: string;
|
|
76
|
+
repoUri?: string;
|
|
77
|
+
}): SarifDocument;
|
|
78
|
+
/**
|
|
79
|
+
* Format SARIF report as pretty JSON string.
|
|
80
|
+
*/
|
|
81
|
+
export declare function formatSarifJson(scanResults: FileScanResult[], options?: {
|
|
82
|
+
toolVersion?: string;
|
|
83
|
+
}): string;
|
|
84
|
+
//# sourceMappingURL=sarif.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sarif.d.ts","sourceRoot":"","sources":["../src/sarif.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,MAAM,WAAW,qBAAqB;IACpC,gBAAgB,EAAE,qBAAqB,CAAC;IACxC,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,gBAAgB,EAAE,qBAAqB,CAAC;CACzC;AAED,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,eAAe,CAAC,EAAE;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,oBAAoB,CAAC,EAAE;QACrB,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;KAC9C,CAAC;IACF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IAC7C,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE;QACJ,MAAM,EAAE;YACN,IAAI,EAAE,MAAM,CAAC;YACb,YAAY,CAAC,EAAE,MAAM,CAAC;YACtB,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,eAAe,CAAC,EAAE,MAAM,CAAC;YACzB,cAAc,CAAC,EAAE,MAAM,CAAC;YACxB,KAAK,EAAE,wBAAwB,EAAE,CAAC;SACnC,CAAC;KACH,CAAC;IACF,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,QAAQ,EAAE,CAAC;CAClB;AA4CD;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,cAAc,EAAE,EAC7B,OAAO,CAAC,EAAE;IACR,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GACA,aAAa,CAmFf;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,cAAc,EAAE,EAAE,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAGzG"}
|
package/dist/sarif.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SARIF (Static Analysis Results Interchange Format) Exporter for SecureFlow CLI (#728)
|
|
3
|
+
*
|
|
4
|
+
* Converts CLI scan results into standardized OASIS SARIF v2.1.0 schema format
|
|
5
|
+
* for direct ingestion by GitHub Advanced Security, GitLab Security Dashboards, and enterprise CI/CD systems.
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_RULE_DEFINITIONS = {
|
|
8
|
+
'environment variable': {
|
|
9
|
+
id: 'SECUREFLOW-001',
|
|
10
|
+
name: 'ConsoleSecretLoggingEnvironmentVariable',
|
|
11
|
+
shortDescription: {
|
|
12
|
+
text: 'Console logging of environment variable containing potential secret',
|
|
13
|
+
},
|
|
14
|
+
fullDescription: {
|
|
15
|
+
text: 'Passing process.env or other environment variable getters into console methods exposes credentials in build logs or standard output.',
|
|
16
|
+
},
|
|
17
|
+
defaultConfiguration: {
|
|
18
|
+
level: 'error',
|
|
19
|
+
},
|
|
20
|
+
helpUri: 'https://github.com/GauravKarakoti/SecureFlow#rules',
|
|
21
|
+
},
|
|
22
|
+
'secret-named identifier': {
|
|
23
|
+
id: 'SECUREFLOW-002',
|
|
24
|
+
name: 'ConsoleSecretLoggingSecretIdentifier',
|
|
25
|
+
shortDescription: {
|
|
26
|
+
text: 'Console logging of secret-named identifier or credential parameter',
|
|
27
|
+
},
|
|
28
|
+
fullDescription: {
|
|
29
|
+
text: 'Identifiers containing secret, password, apikey, token, or auth parameters passed directly into console logging statements.',
|
|
30
|
+
},
|
|
31
|
+
defaultConfiguration: {
|
|
32
|
+
level: 'error',
|
|
33
|
+
},
|
|
34
|
+
helpUri: 'https://github.com/GauravKarakoti/SecureFlow#rules',
|
|
35
|
+
},
|
|
36
|
+
'generic-secret-logging': {
|
|
37
|
+
id: 'SECUREFLOW-000',
|
|
38
|
+
name: 'ConsoleSecretLoggingGeneric',
|
|
39
|
+
shortDescription: {
|
|
40
|
+
text: 'Potential secret credential logging in console output',
|
|
41
|
+
},
|
|
42
|
+
defaultConfiguration: {
|
|
43
|
+
level: 'error',
|
|
44
|
+
},
|
|
45
|
+
helpUri: 'https://github.com/GauravKarakoti/SecureFlow#rules',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Maps CLI scan results into a valid OASIS SARIF v2.1.0 document object.
|
|
50
|
+
*/
|
|
51
|
+
export function generateSarifReport(scanResults, options) {
|
|
52
|
+
const version = options?.toolVersion || '0.1.0';
|
|
53
|
+
const rulesMap = new Map();
|
|
54
|
+
const sarifResults = [];
|
|
55
|
+
for (const fileResult of scanResults) {
|
|
56
|
+
if (!fileResult.violations || fileResult.violations.length === 0) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
for (const violation of fileResult.violations) {
|
|
60
|
+
const reasonKey = violation.reason || 'generic-secret-logging';
|
|
61
|
+
let ruleInfo = rulesMap.get(reasonKey);
|
|
62
|
+
if (!ruleInfo) {
|
|
63
|
+
const descriptor = DEFAULT_RULE_DEFINITIONS[reasonKey] || {
|
|
64
|
+
id: `SECUREFLOW-${rulesMap.size + 100}`,
|
|
65
|
+
name: `ConsoleSecretLoggingCustomRule${rulesMap.size + 1}`,
|
|
66
|
+
shortDescription: {
|
|
67
|
+
text: `Console logging of ${violation.reason}`,
|
|
68
|
+
},
|
|
69
|
+
defaultConfiguration: {
|
|
70
|
+
level: 'error',
|
|
71
|
+
},
|
|
72
|
+
helpUri: 'https://github.com/GauravKarakoti/SecureFlow#rules',
|
|
73
|
+
};
|
|
74
|
+
ruleInfo = { descriptor, index: rulesMap.size };
|
|
75
|
+
rulesMap.set(reasonKey, ruleInfo);
|
|
76
|
+
}
|
|
77
|
+
sarifResults.push({
|
|
78
|
+
ruleId: ruleInfo.descriptor.id,
|
|
79
|
+
ruleIndex: ruleInfo.index,
|
|
80
|
+
level: ruleInfo.descriptor.defaultConfiguration?.level || 'error',
|
|
81
|
+
message: {
|
|
82
|
+
text: `[SecureFlow] ${violation.reason} passed to console call: "${violation.text}"`,
|
|
83
|
+
},
|
|
84
|
+
locations: [
|
|
85
|
+
{
|
|
86
|
+
physicalLocation: {
|
|
87
|
+
artifactLocation: {
|
|
88
|
+
uri: fileResult.path.replace(/\\/g, '/'),
|
|
89
|
+
},
|
|
90
|
+
region: {
|
|
91
|
+
startLine: Math.max(1, violation.line),
|
|
92
|
+
snippet: {
|
|
93
|
+
text: violation.text,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
properties: {
|
|
100
|
+
reason: violation.reason,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const rulesArray = Array.from(rulesMap.values()).map((r) => r.descriptor);
|
|
106
|
+
if (rulesArray.length === 0) {
|
|
107
|
+
// Appended `!` to override 'undefined' error resulting from Record<string, ...> signature
|
|
108
|
+
rulesArray.push(DEFAULT_RULE_DEFINITIONS['generic-secret-logging']);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
112
|
+
version: '2.1.0',
|
|
113
|
+
runs: [
|
|
114
|
+
{
|
|
115
|
+
tool: {
|
|
116
|
+
driver: {
|
|
117
|
+
name: 'SecureFlow CLI',
|
|
118
|
+
semanticVersion: version,
|
|
119
|
+
informationUri: 'https://github.com/GauravKarakoti/SecureFlow',
|
|
120
|
+
rules: rulesArray,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
results: sarifResults,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Format SARIF report as pretty JSON string.
|
|
130
|
+
*/
|
|
131
|
+
export function formatSarifJson(scanResults, options) {
|
|
132
|
+
const sarifDoc = generateSarifReport(scanResults, options);
|
|
133
|
+
return JSON.stringify(sarifDoc, null, 2);
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=sarif.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sarif.js","sourceRoot":"","sources":["../src/sarif.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The secret-logging detector, separated from the CLI shell.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is pure: it takes text and returns violations. The process
|
|
5
|
+
* exit, the git calls and the console output live in `./index` and `./git`, so
|
|
6
|
+
* the part that decides whether a commit is blocked can actually be tested
|
|
7
|
+
* (#593).
|
|
8
|
+
*/
|
|
9
|
+
/** One flagged call site. */
|
|
10
|
+
export interface Violation {
|
|
11
|
+
/** 1-based line of the `console.*` call. */
|
|
12
|
+
line: number;
|
|
13
|
+
/** The source line as written, for the report. */
|
|
14
|
+
text: string;
|
|
15
|
+
/** Which indicator matched, so the message can say why. */
|
|
16
|
+
reason: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Bytes above which a staged blob is skipped.
|
|
20
|
+
*
|
|
21
|
+
* Nothing bounded this before, so a staged fixture or a vendored bundle was
|
|
22
|
+
* read into memory and scanned line by line.
|
|
23
|
+
*/
|
|
24
|
+
export declare const MAX_SCANNED_BYTES: number;
|
|
25
|
+
/** Whether `path` should be scanned at all. */
|
|
26
|
+
export declare function shouldScanFile(path: string, byteLength?: number): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Heuristic for content that is binary despite its name.
|
|
29
|
+
*
|
|
30
|
+
* A NUL byte does not occur in text. Checking this beats trusting the
|
|
31
|
+
* extension, since a staged blob can be anything.
|
|
32
|
+
*/
|
|
33
|
+
export declare function looksBinary(content: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Blank out the *contents* of string literals, preserving length.
|
|
36
|
+
*
|
|
37
|
+
* The previous implementation deleted them outright:
|
|
38
|
+
*
|
|
39
|
+
* trimmedLine.replace(/(["'`])(?:(?=(\\?))\2.)*?\1/g, '')
|
|
40
|
+
*
|
|
41
|
+
* which turned `` console.log(`token: ${authToken}`) `` into `console.log()` —
|
|
42
|
+
* the interpolation went with the quotes, so the single most common way a
|
|
43
|
+
* secret reaches a log line became invisible. Deleting also shifts every column
|
|
44
|
+
* after it, which matters now that whole files are scanned rather than
|
|
45
|
+
* individual lines.
|
|
46
|
+
*
|
|
47
|
+
* Interpolations inside a template literal are left intact, because
|
|
48
|
+
* `${authToken}` is code, not text. Escapes are honoured so a `\"` does not end
|
|
49
|
+
* a literal, and an unterminated quote stops at the newline rather than eating
|
|
50
|
+
* the rest of the file.
|
|
51
|
+
*/
|
|
52
|
+
export declare function maskStringLiterals(source: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Text of the balanced argument list starting at `openParen`, or null when the
|
|
55
|
+
* parenthesis is never closed.
|
|
56
|
+
*
|
|
57
|
+
* Scanning for the matching parenthesis rather than matching a regex is what
|
|
58
|
+
* makes a wrapped call detectable. The old detector tested one line at a time,
|
|
59
|
+
* so the `[\s\S]*?` in its pattern could never span anything and
|
|
60
|
+
*
|
|
61
|
+
* console.log(
|
|
62
|
+
* 'db password:',
|
|
63
|
+
* process.env.DB_PASSWORD
|
|
64
|
+
* );
|
|
65
|
+
*
|
|
66
|
+
* — the shape Prettier produces at default print width — went unflagged.
|
|
67
|
+
*/
|
|
68
|
+
export declare function readArgumentList(source: string, openParen: number): string | null;
|
|
69
|
+
/** 1-based line number of `offset`. */
|
|
70
|
+
export declare function lineOf(source: string, offset: number): number;
|
|
71
|
+
/**
|
|
72
|
+
* Find every `console.*` call whose arguments reference a secret.
|
|
73
|
+
*
|
|
74
|
+
* Operates on the whole file, not line by line, so a call split across lines is
|
|
75
|
+
* matched. The reported line is the line the call *starts* on, which is where a
|
|
76
|
+
* reader will look.
|
|
77
|
+
*/
|
|
78
|
+
export declare function findSecretLogging(source: string): Violation[];
|
|
79
|
+
/** Result of scanning one staged file. */
|
|
80
|
+
export interface FileScanResult {
|
|
81
|
+
path: string;
|
|
82
|
+
violations: Violation[];
|
|
83
|
+
/** Set when the file was not scanned, with the reason. */
|
|
84
|
+
skipped?: string;
|
|
85
|
+
}
|
|
86
|
+
/** Scan one staged blob. */
|
|
87
|
+
export declare function scanFile(path: string, content: string): FileScanResult;
|
|
88
|
+
export type OutputFormat = 'text' | 'json' | 'sarif';
|
|
89
|
+
/**
|
|
90
|
+
* Format scan results based on the chosen output format ('text' | 'json' | 'sarif').
|
|
91
|
+
*/
|
|
92
|
+
export declare function formatScanResults(results: FileScanResult[], format?: OutputFormat): string;
|
|
93
|
+
//# sourceMappingURL=scanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,6BAA6B;AAC7B,MAAM,WAAW,SAAS;IACxB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;CAChB;AA8CD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,QAAa,CAAC;AAE5C,+CAA+C;AAC/C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CASzE;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEpD;AAKD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CA4EzD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAYjF;AAED,uCAAuC;AACvC,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAM7D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,CA2B7D;AAED,0CAA0C;AAC1C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,4BAA4B;AAC5B,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,cAAc,CAUtE;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAErD;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,cAAc,EAAE,EACzB,MAAM,GAAE,YAAqB,GAC5B,MAAM,CAmBR"}
|
package/dist/scanner.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The secret-logging detector, separated from the CLI shell.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is pure: it takes text and returns violations. The process
|
|
5
|
+
* exit, the git calls and the console output live in `./index` and `./git`, so
|
|
6
|
+
* the part that decides whether a commit is blocked can actually be tested
|
|
7
|
+
* (#593).
|
|
8
|
+
*/
|
|
9
|
+
import { formatSarifJson } from './sarif.js';
|
|
10
|
+
/** Console methods that put their arguments somewhere durable. */
|
|
11
|
+
const CONSOLE_METHODS = ['log', 'info', 'warn', 'error', 'debug', 'trace', 'table', 'dir'];
|
|
12
|
+
/**
|
|
13
|
+
* Start of a console call. Whitespace is permitted around the dot and the
|
|
14
|
+
* parenthesis because a formatter will put it there.
|
|
15
|
+
*/
|
|
16
|
+
const CONSOLE_CALL = new RegExp(`console\\s*\\.\\s*(?:${CONSOLE_METHODS.join('|')})\\s*\\(`, 'g');
|
|
17
|
+
/**
|
|
18
|
+
* What makes an argument list suspicious.
|
|
19
|
+
*
|
|
20
|
+
* Checked against the *masked* source, so a string literal that merely contains
|
|
21
|
+
* the word "password" does not match — only an identifier or a member
|
|
22
|
+
* expression does.
|
|
23
|
+
*/
|
|
24
|
+
const INDICATORS = [
|
|
25
|
+
['environment variable', /\b(?:process\s*\.\s*env|import\s*\.\s*meta\s*\.\s*env|Deno\s*\.\s*env|os\s*\.\s*environ)\b/],
|
|
26
|
+
['secret-named identifier', /\b\w*(?:password|passwd|secret|token|credential|apikey|privatekey)\w*\b/i],
|
|
27
|
+
// `key` and `auth` on their own are common enough in ordinary code
|
|
28
|
+
// (`keyof`, `authorised`, `keys`) that they are only flagged when they read
|
|
29
|
+
// as a whole word or as an obvious compound.
|
|
30
|
+
['secret-named identifier', /\b(?:api[_-]?key|access[_-]?key|secret[_-]?key|auth[_-]?token|authorization)\b/i],
|
|
31
|
+
];
|
|
32
|
+
/** Extensions never worth scanning as source text. */
|
|
33
|
+
const BINARY_EXTENSIONS = [
|
|
34
|
+
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.avif', '.bmp', '.tiff',
|
|
35
|
+
'.pdf', '.zip', '.gz', '.tar', '.bz2', '.7z', '.rar',
|
|
36
|
+
'.woff', '.woff2', '.ttf', '.otf', '.eot',
|
|
37
|
+
'.mp3', '.mp4', '.wav', '.mov', '.avi', '.webm',
|
|
38
|
+
'.so', '.dylib', '.dll', '.exe', '.wasm', '.class', '.jar',
|
|
39
|
+
'.sqlite', '.db',
|
|
40
|
+
];
|
|
41
|
+
/** Generated files that are large, uninteresting, and full of hashes. */
|
|
42
|
+
const GENERATED_FILES = [
|
|
43
|
+
'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb', 'composer.lock',
|
|
44
|
+
'Cargo.lock', 'Gemfile.lock', 'poetry.lock',
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Bytes above which a staged blob is skipped.
|
|
48
|
+
*
|
|
49
|
+
* Nothing bounded this before, so a staged fixture or a vendored bundle was
|
|
50
|
+
* read into memory and scanned line by line.
|
|
51
|
+
*/
|
|
52
|
+
export const MAX_SCANNED_BYTES = 512 * 1024;
|
|
53
|
+
/** Whether `path` should be scanned at all. */
|
|
54
|
+
export function shouldScanFile(path, byteLength) {
|
|
55
|
+
const lower = path.toLowerCase();
|
|
56
|
+
const basename = lower.split('/').pop() ?? lower;
|
|
57
|
+
if (GENERATED_FILES.some((name) => basename === name.toLowerCase()))
|
|
58
|
+
return false;
|
|
59
|
+
if (BINARY_EXTENSIONS.some((ext) => lower.endsWith(ext)))
|
|
60
|
+
return false;
|
|
61
|
+
if (typeof byteLength === 'number' && byteLength > MAX_SCANNED_BYTES)
|
|
62
|
+
return false;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Heuristic for content that is binary despite its name.
|
|
67
|
+
*
|
|
68
|
+
* A NUL byte does not occur in text. Checking this beats trusting the
|
|
69
|
+
* extension, since a staged blob can be anything.
|
|
70
|
+
*/
|
|
71
|
+
export function looksBinary(content) {
|
|
72
|
+
return content.includes('\u0000');
|
|
73
|
+
}
|
|
74
|
+
/** Filler used where a string literal's contents were. */
|
|
75
|
+
const MASK_CHAR = '·';
|
|
76
|
+
/**
|
|
77
|
+
* Blank out the *contents* of string literals, preserving length.
|
|
78
|
+
*
|
|
79
|
+
* The previous implementation deleted them outright:
|
|
80
|
+
*
|
|
81
|
+
* trimmedLine.replace(/(["'`])(?:(?=(\\?))\2.)*?\1/g, '')
|
|
82
|
+
*
|
|
83
|
+
* which turned `` console.log(`token: ${authToken}`) `` into `console.log()` —
|
|
84
|
+
* the interpolation went with the quotes, so the single most common way a
|
|
85
|
+
* secret reaches a log line became invisible. Deleting also shifts every column
|
|
86
|
+
* after it, which matters now that whole files are scanned rather than
|
|
87
|
+
* individual lines.
|
|
88
|
+
*
|
|
89
|
+
* Interpolations inside a template literal are left intact, because
|
|
90
|
+
* `${authToken}` is code, not text. Escapes are honoured so a `\"` does not end
|
|
91
|
+
* a literal, and an unterminated quote stops at the newline rather than eating
|
|
92
|
+
* the rest of the file.
|
|
93
|
+
*/
|
|
94
|
+
export function maskStringLiterals(source) {
|
|
95
|
+
const out = source.split('');
|
|
96
|
+
let index = 0;
|
|
97
|
+
while (index < out.length) {
|
|
98
|
+
const char = out[index];
|
|
99
|
+
// Line comments are masked, not merely skipped over: skipping advances the
|
|
100
|
+
// cursor but leaves the text in the output, so the commented-out code was
|
|
101
|
+
// still scanned. The old detector only handled `//` at the very start of a
|
|
102
|
+
// trimmed line, so a trailing comment was scanned as code either way.
|
|
103
|
+
if (char === '/' && out[index + 1] === '/') {
|
|
104
|
+
while (index < out.length && out[index] !== '\n') {
|
|
105
|
+
out[index] = MASK_CHAR;
|
|
106
|
+
index += 1;
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (char === '/' && out[index + 1] === '*') {
|
|
111
|
+
index += 2;
|
|
112
|
+
while (index < out.length && !(out[index] === '*' && out[index + 1] === '/')) {
|
|
113
|
+
if (out[index] !== '\n')
|
|
114
|
+
out[index] = MASK_CHAR;
|
|
115
|
+
index += 1;
|
|
116
|
+
}
|
|
117
|
+
index += 2;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char !== '"' && char !== "'" && char !== '`') {
|
|
121
|
+
index += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const quote = char;
|
|
125
|
+
index += 1;
|
|
126
|
+
while (index < out.length) {
|
|
127
|
+
const current = out[index];
|
|
128
|
+
if (current === '\\') {
|
|
129
|
+
// Escaped character: mask both, and never let it terminate the literal.
|
|
130
|
+
if (out[index] !== '\n')
|
|
131
|
+
out[index] = MASK_CHAR;
|
|
132
|
+
if (index + 1 < out.length && out[index + 1] !== '\n')
|
|
133
|
+
out[index + 1] = MASK_CHAR;
|
|
134
|
+
index += 2;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (current === quote) {
|
|
138
|
+
index += 1;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
// An unterminated quote must not swallow the rest of the file. Only a
|
|
142
|
+
// template literal legally spans lines.
|
|
143
|
+
if (current === '\n' && quote !== '`')
|
|
144
|
+
break;
|
|
145
|
+
// `${ … }` inside a template literal is code. Leave it visible, tracking
|
|
146
|
+
// brace depth so a nested object literal does not end it early.
|
|
147
|
+
if (quote === '`' && current === '$' && out[index + 1] === '{') {
|
|
148
|
+
let depth = 1;
|
|
149
|
+
index += 2;
|
|
150
|
+
while (index < out.length && depth > 0) {
|
|
151
|
+
if (out[index] === '{')
|
|
152
|
+
depth += 1;
|
|
153
|
+
else if (out[index] === '}')
|
|
154
|
+
depth -= 1;
|
|
155
|
+
index += 1;
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (current !== '\n')
|
|
160
|
+
out[index] = MASK_CHAR;
|
|
161
|
+
index += 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out.join('');
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Text of the balanced argument list starting at `openParen`, or null when the
|
|
168
|
+
* parenthesis is never closed.
|
|
169
|
+
*
|
|
170
|
+
* Scanning for the matching parenthesis rather than matching a regex is what
|
|
171
|
+
* makes a wrapped call detectable. The old detector tested one line at a time,
|
|
172
|
+
* so the `[\s\S]*?` in its pattern could never span anything and
|
|
173
|
+
*
|
|
174
|
+
* console.log(
|
|
175
|
+
* 'db password:',
|
|
176
|
+
* process.env.DB_PASSWORD
|
|
177
|
+
* );
|
|
178
|
+
*
|
|
179
|
+
* — the shape Prettier produces at default print width — went unflagged.
|
|
180
|
+
*/
|
|
181
|
+
export function readArgumentList(source, openParen) {
|
|
182
|
+
let depth = 0;
|
|
183
|
+
for (let i = openParen; i < source.length; i += 1) {
|
|
184
|
+
if (source[i] === '(')
|
|
185
|
+
depth += 1;
|
|
186
|
+
else if (source[i] === ')') {
|
|
187
|
+
depth -= 1;
|
|
188
|
+
if (depth === 0)
|
|
189
|
+
return source.slice(openParen + 1, i);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
/** 1-based line number of `offset`. */
|
|
195
|
+
export function lineOf(source, offset) {
|
|
196
|
+
let line = 1;
|
|
197
|
+
for (let i = 0; i < offset && i < source.length; i += 1) {
|
|
198
|
+
if (source[i] === '\n')
|
|
199
|
+
line += 1;
|
|
200
|
+
}
|
|
201
|
+
return line;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Find every `console.*` call whose arguments reference a secret.
|
|
205
|
+
*
|
|
206
|
+
* Operates on the whole file, not line by line, so a call split across lines is
|
|
207
|
+
* matched. The reported line is the line the call *starts* on, which is where a
|
|
208
|
+
* reader will look.
|
|
209
|
+
*/
|
|
210
|
+
export function findSecretLogging(source) {
|
|
211
|
+
if (!source)
|
|
212
|
+
return [];
|
|
213
|
+
const masked = maskStringLiterals(source);
|
|
214
|
+
const lines = source.split(/\r?\n/);
|
|
215
|
+
const violations = [];
|
|
216
|
+
CONSOLE_CALL.lastIndex = 0;
|
|
217
|
+
let match;
|
|
218
|
+
while ((match = CONSOLE_CALL.exec(masked)) !== null) {
|
|
219
|
+
const openParen = match.index + match[0].length - 1;
|
|
220
|
+
const args = readArgumentList(masked, openParen);
|
|
221
|
+
if (args === null)
|
|
222
|
+
continue;
|
|
223
|
+
const indicator = INDICATORS.find(([, pattern]) => pattern.test(args));
|
|
224
|
+
if (!indicator)
|
|
225
|
+
continue;
|
|
226
|
+
const line = lineOf(masked, match.index);
|
|
227
|
+
violations.push({
|
|
228
|
+
line,
|
|
229
|
+
text: (lines[line - 1] ?? '').trim(),
|
|
230
|
+
reason: indicator[0],
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return violations;
|
|
234
|
+
}
|
|
235
|
+
/** Scan one staged blob. */
|
|
236
|
+
export function scanFile(path, content) {
|
|
237
|
+
if (!shouldScanFile(path, Buffer.byteLength(content, 'utf-8'))) {
|
|
238
|
+
return { path, violations: [], skipped: 'excluded by type or size' };
|
|
239
|
+
}
|
|
240
|
+
if (looksBinary(content)) {
|
|
241
|
+
return { path, violations: [], skipped: 'binary content' };
|
|
242
|
+
}
|
|
243
|
+
return { path, violations: findSecretLogging(content) };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Format scan results based on the chosen output format ('text' | 'json' | 'sarif').
|
|
247
|
+
*/
|
|
248
|
+
export function formatScanResults(results, format = 'text') {
|
|
249
|
+
if (format === 'json') {
|
|
250
|
+
return JSON.stringify(results, null, 2);
|
|
251
|
+
}
|
|
252
|
+
if (format === 'sarif') {
|
|
253
|
+
return formatSarifJson(results);
|
|
254
|
+
}
|
|
255
|
+
// Default text summary
|
|
256
|
+
let text = '';
|
|
257
|
+
for (const r of results) {
|
|
258
|
+
for (const v of r.violations) {
|
|
259
|
+
text += `🚨 [SecureFlow] Secret logging detected in ${r.path}:${v.line}\n`;
|
|
260
|
+
text += ` -> ${v.text}\n`;
|
|
261
|
+
text += ` why: ${v.reason} passed to a console call\n`;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return text;
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=scanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanner.js","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAY7C,kEAAkE;AAClE,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;AAE3F;;;GAGG;AACH,MAAM,YAAY,GAAG,IAAI,MAAM,CAC7B,wBAAwB,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAC3D,GAAG,CACJ,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,GAA6C;IAC3D,CAAC,sBAAsB,EAAE,4FAA4F,CAAC;IACtH,CAAC,yBAAyB,EAAE,0EAA0E,CAAC;IACvG,mEAAmE;IACnE,4EAA4E;IAC5E,6CAA6C;IAC7C,CAAC,yBAAyB,EAAE,iFAAiF,CAAC;CAC/G,CAAC;AAEF,sDAAsD;AACtD,MAAM,iBAAiB,GAAG;IACxB,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO;IAC1E,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IACzC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO;IAC/C,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM;IAC1D,SAAS,EAAE,KAAK;CACjB,CAAC;AAEF,yEAAyE;AACzE,MAAM,eAAe,GAAG;IACtB,mBAAmB,EAAE,WAAW,EAAE,gBAAgB,EAAE,WAAW,EAAE,eAAe;IAChF,YAAY,EAAE,cAAc,EAAE,aAAa;CAC5C,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,GAAG,IAAI,CAAC;AAE5C,+CAA+C;AAC/C,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,UAAmB;IAC9D,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,KAAK,CAAC;IAEjD,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC;QAAE,OAAO,KAAK,CAAC;IAClF,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACvE,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,GAAG,iBAAiB;QAAE,OAAO,KAAK,CAAC;IAEnF,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,OAAO,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACpC,CAAC;AAED,0DAA0D;AAC1D,MAAM,SAAS,GAAG,GAAG,CAAC;AAEtB;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAC/C,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC7B,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,OAAO,KAAK,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC;QAExB,2EAA2E;QAC3E,0EAA0E;QAC1E,2EAA2E;QAC3E,sEAAsE;QACtE,IAAI,IAAI,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YAC3C,OAAO,KAAK,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;gBACjD,GAAG,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC;gBACvB,KAAK,IAAI,CAAC,CAAC;YACb,CAAC;YACD,SAAS;QACX,CAAC;QAED,IAAI,IAAI,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YAC3C,KAAK,IAAI,CAAC,CAAC;YACX,OAAO,KAAK,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;gBAC7E,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI;oBAAE,GAAG,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC;gBAChD,KAAK,IAAI,CAAC,CAAC;YACb,CAAC;YACD,KAAK,IAAI,CAAC,CAAC;YACX,SAAS;QACX,CAAC;QAED,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjD,KAAK,IAAI,CAAC,CAAC;YACX,SAAS;QACX,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC;QACnB,KAAK,IAAI,CAAC,CAAC;QAEX,OAAO,KAAK,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;YAC1B,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC;YAE3B,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrB,wEAAwE;gBACxE,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI;oBAAE,GAAG,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC;gBAChD,IAAI,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI;oBAAE,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC;gBAClF,KAAK,IAAI,CAAC,CAAC;gBACX,SAAS;YACX,CAAC;YAED,IAAI,OAAO,KAAK,KAAK,EAAE,CAAC;gBACtB,KAAK,IAAI,CAAC,CAAC;gBACX,MAAM;YACR,CAAC;YAED,sEAAsE;YACtE,wCAAwC;YACxC,IAAI,OAAO,KAAK,IAAI,IAAI,KAAK,KAAK,GAAG;gBAAE,MAAM;YAE7C,yEAAyE;YACzE,gEAAgE;YAChE,IAAI,KAAK,KAAK,GAAG,IAAI,OAAO,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC/D,IAAI,KAAK,GAAG,CAAC,CAAC;gBACd,KAAK,IAAI,CAAC,CAAC;gBACX,OAAO,KAAK,GAAG,GAAG,CAAC,MAAM,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;oBACvC,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,GAAG;wBAAE,KAAK,IAAI,CAAC,CAAC;yBAC9B,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,GAAG;wBAAE,KAAK,IAAI,CAAC,CAAC;oBACxC,KAAK,IAAI,CAAC,CAAC;gBACb,CAAC;gBACD,SAAS;YACX,CAAC;YAED,IAAI,OAAO,KAAK,IAAI;gBAAE,GAAG,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC;YAC7C,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACtB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,SAAiB;IAChE,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,KAAK,IAAI,CAAC,GAAG,SAAS,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAClD,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;YAAE,KAAK,IAAI,CAAC,CAAC;aAC7B,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YAC3B,KAAK,IAAI,CAAC,CAAC;YACX,IAAI,KAAK,KAAK,CAAC;gBAAE,OAAO,MAAM,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,MAAM,CAAC,MAAc,EAAE,MAAc;IACnD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACxD,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI;YAAE,IAAI,IAAI,CAAC,CAAC;IACpC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IAEvB,MAAM,MAAM,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,UAAU,GAAgB,EAAE,CAAC;IAEnC,YAAY,CAAC,SAAS,GAAG,CAAC,CAAC;IAC3B,IAAI,KAA6B,CAAC;IAElC,OAAO,CAAC,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACpD,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,gBAAgB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QACjD,IAAI,IAAI,KAAK,IAAI;YAAE,SAAS;QAE5B,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACvE,IAAI,CAAC,SAAS;YAAE,SAAS;QAEzB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QACzC,UAAU,CAAC,IAAI,CAAC;YACd,IAAI;YACJ,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;YACpC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;SACrB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAUD,4BAA4B;AAC5B,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,OAAe;IACpD,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC;QAC/D,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC;IACvE,CAAC;IAED,IAAI,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC7D,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAAE,CAAC;AAC1D,CAAC;AAID;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAyB,EACzB,MAAM,GAAiB,MAAM;IAE7B,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;QACvB,OAAO,eAAe,CAAC,OAAO,CAAC,CAAC;IAClC,CAAC;IAED,uBAAuB;IACvB,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC;YAC7B,IAAI,IAAI,8CAA8C,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC;YAC3E,IAAI,IAAI,SAAS,CAAC,CAAC,IAAI,IAAI,CAAC;YAC5B,IAAI,IAAI,WAAW,CAAC,CAAC,MAAM,6BAA6B,CAAC;QAC3D,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,32 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "secureflow-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Pre-commit secret logging detector for secure development workflows",
|
|
5
|
-
"main": "./dist/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"secureflow": "./dist/index.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "tsc",
|
|
11
|
-
"prepare": "npm run build",
|
|
12
|
-
"publish": "npm publish --access public"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "secureflow-cli",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Pre-commit secret logging detector for secure development workflows",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"secureflow": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"prepare": "npm run build",
|
|
12
|
+
"publish": "npm publish --access public",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"security",
|
|
18
|
+
"secrets",
|
|
19
|
+
"scanner",
|
|
20
|
+
"pre-commit",
|
|
21
|
+
"husky",
|
|
22
|
+
"secureflow"
|
|
23
|
+
],
|
|
24
|
+
"author": "Gaurav Karakoti",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^26.4.0",
|
|
32
|
+
"typescript": "^6",
|
|
33
|
+
"vitest": "^4.1.11"
|
|
34
|
+
}
|
|
35
|
+
}
|