git-cli-scanner 1.1.2 → 1.1.4
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/cli.js +130 -116
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -538,117 +538,131 @@ var init_logger = __esm({
|
|
|
538
538
|
// src/utils/explorer.ts
|
|
539
539
|
var explorer_exports = {};
|
|
540
540
|
__export(explorer_exports, {
|
|
541
|
-
|
|
541
|
+
runExplorer: () => runExplorer
|
|
542
542
|
});
|
|
543
|
-
|
|
543
|
+
async function scanFile(filePath, baseDir) {
|
|
544
|
+
console.log(`
|
|
545
|
+
${import_picocolors3.default.dim(" Scanning")} ${import_picocolors3.default.cyan(path2.basename(filePath))}${import_picocolors3.default.dim("...")}`);
|
|
546
|
+
const issues = await scanDirectory(path2.dirname(filePath));
|
|
547
|
+
const relativePath = path2.relative(baseDir, filePath).replace(/\\/g, "/");
|
|
548
|
+
const fileIssues = issues.filter((i) => {
|
|
549
|
+
const issuePath = i.file.replace(/\\/g, "/");
|
|
550
|
+
return issuePath === relativePath || issuePath === path2.basename(filePath);
|
|
551
|
+
});
|
|
552
|
+
if (fileIssues.length === 0) {
|
|
553
|
+
console.log(import_picocolors3.default.green("\n No vulnerabilities found in this file.\n"));
|
|
554
|
+
} else {
|
|
555
|
+
console.log(`
|
|
556
|
+
${import_picocolors3.default.bold(`Found ${fileIssues.length} issue(s):`)}
|
|
557
|
+
`);
|
|
558
|
+
fileIssues.forEach((issue) => {
|
|
559
|
+
const severity = issue.severity === "high" ? import_picocolors3.default.red("HIGH") : issue.severity === "medium" ? import_picocolors3.default.yellow("MEDIUM") : import_picocolors3.default.dim("DUMMY");
|
|
560
|
+
const indicator = issue.severity === "dummy" ? import_picocolors3.default.dim("\u25CB") : "\u25CF";
|
|
561
|
+
console.log(` ${indicator} ${severity} ${import_picocolors3.default.dim("\xB7")} ${issue.type}`);
|
|
562
|
+
console.log(` ${import_picocolors3.default.dim("Line:")} ${issue.line || "?"}`);
|
|
563
|
+
console.log(` ${import_picocolors3.default.dim("Match:")} ${issue.match.substring(0, 60)}`);
|
|
564
|
+
if (issue.solution) {
|
|
565
|
+
console.log(` ${import_picocolors3.default.dim("Fix:")} ${import_picocolors3.default.green(issue.solution)}`);
|
|
566
|
+
}
|
|
567
|
+
console.log();
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function buildChoices(dirPath) {
|
|
572
|
+
const choices = [];
|
|
573
|
+
choices.push({
|
|
574
|
+
name: import_picocolors3.default.dim(".. (go back)"),
|
|
575
|
+
value: "__BACK__"
|
|
576
|
+
});
|
|
577
|
+
let entries = [];
|
|
578
|
+
try {
|
|
579
|
+
entries = fs2.readdirSync(dirPath, { withFileTypes: true });
|
|
580
|
+
} catch (err) {
|
|
581
|
+
return choices;
|
|
582
|
+
}
|
|
583
|
+
entries = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules");
|
|
584
|
+
entries.sort((a, b) => {
|
|
585
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
586
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
587
|
+
return a.name.localeCompare(b.name);
|
|
588
|
+
});
|
|
589
|
+
for (const entry of entries) {
|
|
590
|
+
if (entry.isDirectory()) {
|
|
591
|
+
choices.push({
|
|
592
|
+
name: `${import_picocolors3.default.blue("/")}${entry.name}`,
|
|
593
|
+
value: path2.join(dirPath, entry.name),
|
|
594
|
+
description: "folder"
|
|
595
|
+
});
|
|
596
|
+
} else {
|
|
597
|
+
choices.push({
|
|
598
|
+
name: ` ${entry.name}`,
|
|
599
|
+
value: path2.join(dirPath, entry.name),
|
|
600
|
+
description: `${(fs2.statSync(path2.join(dirPath, entry.name)).size / 1024).toFixed(1)} KB`
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
choices.push({
|
|
605
|
+
name: import_picocolors3.default.dim("Exit explorer"),
|
|
606
|
+
value: "__EXIT__"
|
|
607
|
+
});
|
|
608
|
+
return choices;
|
|
609
|
+
}
|
|
610
|
+
async function runExplorer(startDir) {
|
|
611
|
+
let currentDir = path2.resolve(startDir);
|
|
612
|
+
const baseDir = currentDir;
|
|
613
|
+
console.log(`
|
|
614
|
+
${import_picocolors3.default.bold("Git CLI Scanner")} ${import_picocolors3.default.dim("\xB7 Interactive Explorer")}`);
|
|
615
|
+
console.log(import_picocolors3.default.dim(" Use arrow keys to navigate, Enter to select\n"));
|
|
616
|
+
while (true) {
|
|
617
|
+
const choices = buildChoices(currentDir);
|
|
618
|
+
let selected;
|
|
619
|
+
try {
|
|
620
|
+
selected = await (0, import_prompts2.select)({
|
|
621
|
+
message: `${import_picocolors3.default.cyan(currentDir)}`,
|
|
622
|
+
choices,
|
|
623
|
+
pageSize: 20,
|
|
624
|
+
loop: false
|
|
625
|
+
});
|
|
626
|
+
} catch (err) {
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
if (selected === "__EXIT__") {
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
if (selected === "__BACK__") {
|
|
633
|
+
currentDir = path2.dirname(currentDir);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
const stat = fs2.statSync(selected);
|
|
638
|
+
if (stat.isDirectory()) {
|
|
639
|
+
currentDir = selected;
|
|
640
|
+
} else {
|
|
641
|
+
await scanFile(selected, baseDir);
|
|
642
|
+
const next = await (0, import_prompts2.select)({
|
|
643
|
+
message: "What next?",
|
|
644
|
+
choices: [
|
|
645
|
+
{ name: "Continue browsing", value: "continue" },
|
|
646
|
+
{ name: "Exit explorer", value: "exit" }
|
|
647
|
+
]
|
|
648
|
+
});
|
|
649
|
+
if (next === "exit") break;
|
|
650
|
+
}
|
|
651
|
+
} catch (err) {
|
|
652
|
+
console.log(import_picocolors3.default.red(` Cannot access: ${selected}`));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
console.log(import_picocolors3.default.dim("\n Explorer closed.\n"));
|
|
656
|
+
}
|
|
657
|
+
var import_prompts2, import_picocolors3, fs2, path2;
|
|
544
658
|
var init_explorer = __esm({
|
|
545
659
|
"src/utils/explorer.ts"() {
|
|
546
660
|
"use strict";
|
|
547
|
-
|
|
661
|
+
import_prompts2 = require("@inquirer/prompts");
|
|
548
662
|
import_picocolors3 = __toESM(require("picocolors"));
|
|
549
663
|
fs2 = __toESM(require("fs"));
|
|
550
664
|
path2 = __toESM(require("path"));
|
|
551
665
|
init_scanner();
|
|
552
|
-
explorePrompt = (0, import_core.createPrompt)(
|
|
553
|
-
(config, done) => {
|
|
554
|
-
const [currentPath, setCurrentPath] = (0, import_core.useState)(config.currentDir);
|
|
555
|
-
const [selectedIndex, setSelectedIndex] = (0, import_core.useState)(0);
|
|
556
|
-
const [scanResult, setScanResult] = (0, import_core.useState)(null);
|
|
557
|
-
const [scanningFile, setScanningFile] = (0, import_core.useState)(null);
|
|
558
|
-
const prefix = (0, import_core.usePrefix)({ status: "idle" });
|
|
559
|
-
let items = [];
|
|
560
|
-
try {
|
|
561
|
-
items = fs2.readdirSync(currentPath, { withFileTypes: true });
|
|
562
|
-
} catch (err) {
|
|
563
|
-
}
|
|
564
|
-
items.sort((a, b) => {
|
|
565
|
-
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
566
|
-
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
567
|
-
return a.name.localeCompare(b.name);
|
|
568
|
-
});
|
|
569
|
-
const choices = [
|
|
570
|
-
{ name: "\u{1F519} Go Back (or press Left Arrow)", type: "back" },
|
|
571
|
-
...items.map((i) => ({
|
|
572
|
-
name: i.isDirectory() ? `\u{1F4C1} ${i.name}` : `\u{1F4C4} ${i.name}`,
|
|
573
|
-
type: i.isDirectory() ? "dir" : "file",
|
|
574
|
-
nameRaw: i.name
|
|
575
|
-
}))
|
|
576
|
-
];
|
|
577
|
-
(0, import_core.useKeypress)(async (key, rl) => {
|
|
578
|
-
if ((0, import_core.isEnterKey)(key) || key.name === "right") {
|
|
579
|
-
const selected = choices[selectedIndex];
|
|
580
|
-
if (selected.type === "back") {
|
|
581
|
-
setCurrentPath(path2.dirname(currentPath));
|
|
582
|
-
setSelectedIndex(0);
|
|
583
|
-
setScanResult(null);
|
|
584
|
-
} else if (selected.type === "dir") {
|
|
585
|
-
setCurrentPath(path2.join(currentPath, selected.nameRaw));
|
|
586
|
-
setSelectedIndex(0);
|
|
587
|
-
setScanResult(null);
|
|
588
|
-
} else if (selected.type === "file") {
|
|
589
|
-
const fullPath = path2.join(currentPath, selected.nameRaw);
|
|
590
|
-
setScanningFile(fullPath);
|
|
591
|
-
setScanResult(null);
|
|
592
|
-
const issues = await scanDirectory(currentPath);
|
|
593
|
-
const fileIssues = issues.filter((i) => i.file === path2.relative(process.cwd(), fullPath).replace(/\\/g, "/"));
|
|
594
|
-
setScanResult(fileIssues);
|
|
595
|
-
setScanningFile(null);
|
|
596
|
-
}
|
|
597
|
-
} else if (key.name === "left") {
|
|
598
|
-
setCurrentPath(path2.dirname(currentPath));
|
|
599
|
-
setSelectedIndex(0);
|
|
600
|
-
setScanResult(null);
|
|
601
|
-
} else if ((0, import_core.isUpKey)(key)) {
|
|
602
|
-
setSelectedIndex((prev) => prev > 0 ? prev - 1 : choices.length - 1);
|
|
603
|
-
setScanResult(null);
|
|
604
|
-
} else if ((0, import_core.isDownKey)(key)) {
|
|
605
|
-
setSelectedIndex((prev) => prev < choices.length - 1 ? prev + 1 : 0);
|
|
606
|
-
setScanResult(null);
|
|
607
|
-
} else if (key.name === "c" && key.ctrl) {
|
|
608
|
-
done("");
|
|
609
|
-
}
|
|
610
|
-
});
|
|
611
|
-
const page = (0, import_core.usePagination)({
|
|
612
|
-
items: choices,
|
|
613
|
-
active: selectedIndex,
|
|
614
|
-
renderItem: ({ item, isActive }) => {
|
|
615
|
-
if (isActive) {
|
|
616
|
-
return import_picocolors3.default.cyan(`\u276F ${item.name}`);
|
|
617
|
-
}
|
|
618
|
-
return ` ${item.name}`;
|
|
619
|
-
},
|
|
620
|
-
pageSize: 15,
|
|
621
|
-
loop: false
|
|
622
|
-
});
|
|
623
|
-
let message = `${prefix} ${import_picocolors3.default.bold("Exploring:")} ${import_picocolors3.default.cyan(currentPath)}
|
|
624
|
-
|
|
625
|
-
${page}`;
|
|
626
|
-
if (scanResult) {
|
|
627
|
-
message += `
|
|
628
|
-
|
|
629
|
-
${import_picocolors3.default.bold("Scan Results for " + choices[selectedIndex].nameRaw + ":")}
|
|
630
|
-
`;
|
|
631
|
-
if (scanResult.length === 0) {
|
|
632
|
-
message += import_picocolors3.default.green("\u2714 No vulnerabilities found in this file.\n");
|
|
633
|
-
} else {
|
|
634
|
-
scanResult.forEach((issue) => {
|
|
635
|
-
message += ` ${issue.severity === "high" ? import_picocolors3.default.red("\u25CF HIGH") : issue.severity === "medium" ? import_picocolors3.default.yellow("\u25CF MEDIUM") : import_picocolors3.default.dim("\u25CB DUMMY")} \xB7 ${issue.type}
|
|
636
|
-
`;
|
|
637
|
-
if (issue.solution) {
|
|
638
|
-
message += ` Fix: ${import_picocolors3.default.green(issue.solution)}
|
|
639
|
-
`;
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
} else if (scanningFile) {
|
|
644
|
-
message += `
|
|
645
|
-
|
|
646
|
-
${import_picocolors3.default.yellow("Scanning...")} (Please wait)
|
|
647
|
-
`;
|
|
648
|
-
}
|
|
649
|
-
return message;
|
|
650
|
-
}
|
|
651
|
-
);
|
|
652
666
|
}
|
|
653
667
|
});
|
|
654
668
|
|
|
@@ -659,9 +673,9 @@ init_logger();
|
|
|
659
673
|
|
|
660
674
|
// src/utils/prompts.ts
|
|
661
675
|
var import_prompts = require("@inquirer/prompts");
|
|
662
|
-
async function askToContinue() {
|
|
676
|
+
async function askToContinue(isHook = true) {
|
|
663
677
|
return await (0, import_prompts.confirm)({
|
|
664
|
-
message: "Vulnerabilities were found! Do you still want to continue with the commit?",
|
|
678
|
+
message: isHook ? "Vulnerabilities were found! Do you still want to continue with the commit?" : "Vulnerabilities were found! Do you want to ignore them?",
|
|
665
679
|
default: false
|
|
666
680
|
});
|
|
667
681
|
}
|
|
@@ -713,7 +727,7 @@ var Spinner = class {
|
|
|
713
727
|
var fs3 = __toESM(require("fs"));
|
|
714
728
|
var path3 = __toESM(require("path"));
|
|
715
729
|
var program = new import_commander.Command();
|
|
716
|
-
program.name("git-cli-scanner").description("Interactive Git hooks vulnerability scanner").version("1.1.
|
|
730
|
+
program.name("git-cli-scanner").description("Interactive Git hooks vulnerability scanner").version("1.1.4");
|
|
717
731
|
program.command("init").description("Install pre-commit hook for automatic scanning").action(() => {
|
|
718
732
|
info("Setting up pre-commit hook...");
|
|
719
733
|
try {
|
|
@@ -731,7 +745,7 @@ program.command("init").description("Install pre-commit hook for automatic scann
|
|
|
731
745
|
# git-cli-scanner pre-commit hook
|
|
732
746
|
# exec < /dev/tty is required to allow interactive prompts in git hooks
|
|
733
747
|
exec < /dev/tty
|
|
734
|
-
npx git-cli-scanner scan
|
|
748
|
+
npx git-cli-scanner scan --hook
|
|
735
749
|
`;
|
|
736
750
|
fs3.writeFileSync(hookPath, hookContent, { mode: 493 });
|
|
737
751
|
success("Pre-commit hook installed!");
|
|
@@ -762,7 +776,7 @@ program.command("disable").description("Remove the pre-commit hook and disable a
|
|
|
762
776
|
error(`Failed to disable: ${err.message}`);
|
|
763
777
|
}
|
|
764
778
|
});
|
|
765
|
-
program.command("scan").description("
|
|
779
|
+
program.command("scan").description("Scan staged files for vulnerabilities").option("--show-sol", "Show solutions for vulnerabilities").option("--hook", "Internal flag used when running as a git hook").action(async (options) => {
|
|
766
780
|
info("Git CLI Scanner running...");
|
|
767
781
|
try {
|
|
768
782
|
const spinner = new Spinner([
|
|
@@ -773,7 +787,7 @@ program.command("scan").description("Interactive vulnerability scan for staged f
|
|
|
773
787
|
"Thinking..."
|
|
774
788
|
]);
|
|
775
789
|
spinner.start();
|
|
776
|
-
await new Promise((
|
|
790
|
+
await new Promise((resolve2) => setTimeout(resolve2, 3e3));
|
|
777
791
|
const issues = await scanDiff();
|
|
778
792
|
if (issues.length > 0) {
|
|
779
793
|
const blockerIssues = issues.filter((i) => i.severity !== "dummy");
|
|
@@ -781,15 +795,15 @@ program.command("scan").description("Interactive vulnerability scan for staged f
|
|
|
781
795
|
const { printIssues: printIssues2 } = (init_logger(), __toCommonJS(logger_exports));
|
|
782
796
|
printIssues2(issues, options.showSol);
|
|
783
797
|
if (blockerIssues.length === 0) {
|
|
784
|
-
info("No high or medium vulnerabilities found. Safe to commit!");
|
|
798
|
+
info(options.hook ? "No high or medium vulnerabilities found. Safe to commit!" : "No high or medium vulnerabilities found.");
|
|
785
799
|
process.exit(0);
|
|
786
800
|
}
|
|
787
|
-
const shouldContinue = await askToContinue();
|
|
801
|
+
const shouldContinue = await askToContinue(options.hook);
|
|
788
802
|
if (shouldContinue) {
|
|
789
|
-
info("Proceeding with commit despite vulnerabilities.");
|
|
803
|
+
info(options.hook ? "Proceeding with commit despite vulnerabilities." : "Proceeding despite vulnerabilities.");
|
|
790
804
|
process.exit(0);
|
|
791
805
|
} else {
|
|
792
|
-
error("Commit aborted. Please edit your files and try again.");
|
|
806
|
+
error(options.hook ? "Commit aborted. Please edit your files and try again." : "Scan aborted. Please fix the issues.");
|
|
793
807
|
process.exit(1);
|
|
794
808
|
}
|
|
795
809
|
} else {
|
|
@@ -818,7 +832,7 @@ program.command("scan-all [dir]").description("Scan an entire directory or codeb
|
|
|
818
832
|
]);
|
|
819
833
|
spinner.start();
|
|
820
834
|
const { scanDirectory: scanDirectory2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
|
|
821
|
-
await new Promise((
|
|
835
|
+
await new Promise((resolve2) => setTimeout(resolve2, 2e3));
|
|
822
836
|
const issues = await scanDirectory2(scanDir);
|
|
823
837
|
if (issues.length > 0) {
|
|
824
838
|
const blockerIssues = issues.filter((i) => i.severity !== "dummy");
|
|
@@ -841,8 +855,8 @@ program.command("scan-all [dir]").description("Scan an entire directory or codeb
|
|
|
841
855
|
});
|
|
842
856
|
program.command("explore").description("Launch an interactive Terminal UI to browse and scan files").action(async () => {
|
|
843
857
|
try {
|
|
844
|
-
const {
|
|
845
|
-
await
|
|
858
|
+
const { runExplorer: runExplorer2 } = (init_explorer(), __toCommonJS(explorer_exports));
|
|
859
|
+
await runExplorer2(process.cwd());
|
|
846
860
|
process.exit(0);
|
|
847
861
|
} catch (err) {
|
|
848
862
|
if (err.name === "ExitPromptError" || err.message?.includes("closed")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-cli-scanner",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "A powerful interactive CLI tool that scans your codebase for hardcoded secrets, API keys, passwords, and private keys before they reach your Git history.",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"scripts": {
|