@re3se/gitsniffer 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/README.md +67 -0
- package/bin/gitsniffer.js +2 -0
- package/package.json +32 -0
- package/src/config.js +32 -0
- package/src/git.js +69 -0
- package/src/index.js +47 -0
- package/src/reporter.js +52 -0
- package/src/scanner.js +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# GitSniffer
|
|
2
|
+
### Smart Code Sentry for Your Terminal
|
|
3
|
+
|
|
4
|
+
> "If it's not clean, it's not finished."
|
|
5
|
+
|
|
6
|
+
GitSniffer is a CLI tool designed to act as an intelligent filter for your codebase. It ensures that no debug leftovers, private keys, or sloppy comments make their way into your repository. It's not just a linter, it's a gatekeeper for quality.
|
|
7
|
+
|
|
8
|
+
## 🚀 The Philosophy
|
|
9
|
+
I build ecosystems where quality is non-negotiable. GitSniffer was born from a simple need: **intentionality**.
|
|
10
|
+
|
|
11
|
+
* **Security First**: Prevent API key leaks before they happen.
|
|
12
|
+
* **Clean Code**: Stop `console.log` and `debugger` statements from polluting production.
|
|
13
|
+
* **Efficiency**: Catch errors in the staging area, seconds before the commit.
|
|
14
|
+
|
|
15
|
+
## ⚙️ How It Works
|
|
16
|
+
GitSniffer hooks into your workflow at the most critical moment: **pre-commit**.
|
|
17
|
+
|
|
18
|
+
1. **Scans**: It analyzes *only* your staged changes (`git diff --cached`).
|
|
19
|
+
2. **Sniffs**: Applies regex-based heuristics to detect code smells and security risks.
|
|
20
|
+
3. **Blocks**: If it finds a critical error (like a private key), it stops the commit.
|
|
21
|
+
|
|
22
|
+
## 🔧 Installation
|
|
23
|
+
Install it globally to use it across all your projects:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g gitsniffer
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 🚀 Usage
|
|
30
|
+
Run it manually in any git repository:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
gitsniffer --run
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Automate with Git Hooks
|
|
37
|
+
To enforce quality standards automatically, add it to your pre-commit hook.
|
|
38
|
+
|
|
39
|
+
**Option 1: Raw Git Hook**
|
|
40
|
+
Add this to `.git/hooks/pre-commit` and make it executable (`chmod +x`):
|
|
41
|
+
```bash
|
|
42
|
+
#!/bin/sh
|
|
43
|
+
gitsniffer --run
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Option 2: Husky**
|
|
47
|
+
If you use Husky in your project:
|
|
48
|
+
```bash
|
|
49
|
+
npx husky add .husky/pre-commit "gitsniffer --run"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 🛡️ Default Rules
|
|
53
|
+
GitSniffer comes pre-configured with a zero-tolerance policy for:
|
|
54
|
+
|
|
55
|
+
* 🔴 **Private Keys** (AWS, RSA, generic private keys) -> `[ERROR]` (Blocks commit)
|
|
56
|
+
* 🔴 **Debugger Statements** -> `[ERROR]` (Blocks commit)
|
|
57
|
+
* 🟡 **Console Logs** -> `[WARNING]`
|
|
58
|
+
* 🔵 **TODO Comments** -> `[INFO]`
|
|
59
|
+
|
|
60
|
+
## 🛠️ Tech Stack
|
|
61
|
+
Built with intentionality using:
|
|
62
|
+
* **Node.js**
|
|
63
|
+
* **Commander.js**
|
|
64
|
+
* **Execa**
|
|
65
|
+
* **Chalk**
|
|
66
|
+
|
|
67
|
+
---
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@re3se/gitsniffer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An intelligent CLI tool that prevents bad code and security leaks from entering your repository.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"gitsniffer": "./bin/gitsniffer.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"git",
|
|
16
|
+
"cli",
|
|
17
|
+
"linter",
|
|
18
|
+
"security",
|
|
19
|
+
"clean-code",
|
|
20
|
+
"pre-commit"
|
|
21
|
+
],
|
|
22
|
+
"author": "re3se",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"chalk": "^5.3.0",
|
|
26
|
+
"commander": "^12.0.0",
|
|
27
|
+
"execa": "^8.0.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const defaultRules = [
|
|
2
|
+
{
|
|
3
|
+
id: 'no-console',
|
|
4
|
+
message: 'Found console.log usage',
|
|
5
|
+
regex: /^(?!\s*\/\/).*console\.log\(/,
|
|
6
|
+
severity: 'warning'
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: 'no-todo',
|
|
10
|
+
message: 'Found TODO comment',
|
|
11
|
+
regex: /\/\/\s*TODO:/i,
|
|
12
|
+
severity: 'info'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'no-private-keys',
|
|
16
|
+
message: 'Potential private key found',
|
|
17
|
+
regex: /-----BEGIN PRIVATE KEY-----/,
|
|
18
|
+
severity: 'error'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'no-aws-keys',
|
|
22
|
+
message: 'Potential AWS Access Key found',
|
|
23
|
+
regex: /(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}/,
|
|
24
|
+
severity: 'error'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'no-debugger',
|
|
28
|
+
message: 'Found debugger statement',
|
|
29
|
+
regex: /debugger;?/,
|
|
30
|
+
severity: 'error'
|
|
31
|
+
}
|
|
32
|
+
];
|
package/src/git.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { extname } from 'path';
|
|
3
|
+
|
|
4
|
+
const BINARY_EXTENSIONS = new Set([
|
|
5
|
+
'.png', '.jpg', '.jpeg', '.gif', '.ico',
|
|
6
|
+
'.pdf', '.zip', '.tar', '.gz', '.7z',
|
|
7
|
+
'.exe', '.dll', '.so', '.dylib', '.bin',
|
|
8
|
+
'.mp3', '.mp4', '.mov', '.avi', '.woff', '.woff2', '.ttf', '.eot'
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export async function getGitDiff() {
|
|
12
|
+
try {
|
|
13
|
+
const { stdout: stagedFiles } = await execa('git', ['diff', '--name-only', '--cached']);
|
|
14
|
+
|
|
15
|
+
const { stdout: diffOutput } = await execa('git', ['diff', '--cached', '--unified=0']);
|
|
16
|
+
|
|
17
|
+
return parseDiff(diffOutput);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('Error executing git command:', error.message);
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseDiff(diffOutput) {
|
|
25
|
+
const files = [];
|
|
26
|
+
let currentFile = null;
|
|
27
|
+
|
|
28
|
+
const lines = diffOutput.split('\n');
|
|
29
|
+
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
if (line.startsWith('diff --git')) {
|
|
32
|
+
const match = line.match(/b\/(.*)$/);
|
|
33
|
+
if (match) {
|
|
34
|
+
const filePath = match[1];
|
|
35
|
+
|
|
36
|
+
if (isBinaryFile(filePath)) {
|
|
37
|
+
currentFile = null;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
currentFile = {
|
|
42
|
+
path: filePath,
|
|
43
|
+
changes: []
|
|
44
|
+
};
|
|
45
|
+
files.push(currentFile);
|
|
46
|
+
}
|
|
47
|
+
} else if (line.startsWith('@@')) {
|
|
48
|
+
const match = line.match(/\+(\d+)(?:,(\d+))?/);
|
|
49
|
+
if (match && currentFile) {
|
|
50
|
+
currentFile.currentLineNumber = parseInt(match[1], 10);
|
|
51
|
+
}
|
|
52
|
+
} else if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
53
|
+
if (currentFile && currentFile.currentLineNumber !== undefined) {
|
|
54
|
+
currentFile.changes.push({
|
|
55
|
+
line: currentFile.currentLineNumber,
|
|
56
|
+
content: line.substring(1)
|
|
57
|
+
});
|
|
58
|
+
currentFile.currentLineNumber++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isBinaryFile(filePath) {
|
|
67
|
+
const ext = extname(filePath).toLowerCase();
|
|
68
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
69
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { getGitDiff } from './git.js';
|
|
6
|
+
import { scanCode } from './scanner.js';
|
|
7
|
+
import { reportIssues } from './reporter.js';
|
|
8
|
+
import { defaultRules } from './config.js';
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
17
|
+
|
|
18
|
+
const program = new Command();
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.name('gitsniffer')
|
|
22
|
+
.description('Smart CLI tool to prevent committing bad code')
|
|
23
|
+
.version(packageJson.version);
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.option('-r, --run', 'Run the sniffer on staged files')
|
|
27
|
+
.action(async (options) => {
|
|
28
|
+
if (options.run) {
|
|
29
|
+
console.log(chalk.cyan('Starting code analysis...'));
|
|
30
|
+
|
|
31
|
+
const files = await getGitDiff();
|
|
32
|
+
|
|
33
|
+
if (files.length === 0) {
|
|
34
|
+
console.log(chalk.green('No staged changes detected.'));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const issues = scanCode(files, defaultRules);
|
|
39
|
+
const exitCode = reportIssues(issues);
|
|
40
|
+
|
|
41
|
+
process.exit(exitCode);
|
|
42
|
+
} else {
|
|
43
|
+
program.help();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
program.parse(process.argv);
|
package/src/reporter.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export function reportIssues(issues) {
|
|
4
|
+
if (issues.length === 0) {
|
|
5
|
+
console.log(chalk.green('No issues found.'));
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
console.log(chalk.bold.underline(`Found ${issues.length} potential issues:\n`));
|
|
10
|
+
|
|
11
|
+
let errorCount = 0;
|
|
12
|
+
let warningCount = 0;
|
|
13
|
+
|
|
14
|
+
issues.forEach(issue => {
|
|
15
|
+
const location = chalk.gray(`${issue.file}:${issue.line}`);
|
|
16
|
+
let severityLabel;
|
|
17
|
+
let messageColor;
|
|
18
|
+
|
|
19
|
+
switch (issue.severity) {
|
|
20
|
+
case 'error':
|
|
21
|
+
severityLabel = chalk.red('[ERROR]');
|
|
22
|
+
messageColor = chalk.red;
|
|
23
|
+
errorCount++;
|
|
24
|
+
break;
|
|
25
|
+
case 'warning':
|
|
26
|
+
severityLabel = chalk.yellow('[WARNING]');
|
|
27
|
+
messageColor = chalk.yellow;
|
|
28
|
+
warningCount++;
|
|
29
|
+
break;
|
|
30
|
+
case 'info':
|
|
31
|
+
severityLabel = chalk.blue('[INFO]');
|
|
32
|
+
messageColor = chalk.blue;
|
|
33
|
+
break;
|
|
34
|
+
default:
|
|
35
|
+
severityLabel = chalk.white('[UNKNOWN]');
|
|
36
|
+
messageColor = chalk.white;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(` ${severityLabel} ${messageColor(issue.message)}`);
|
|
40
|
+
console.log(` Location: ${location}`);
|
|
41
|
+
console.log(` Code: ${chalk.gray(issue.content)}`);
|
|
42
|
+
console.log('');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (errorCount > 0) {
|
|
46
|
+
console.log(chalk.red.bold(`Analysis complete: ${errorCount} errors and ${warningCount} warnings found.`));
|
|
47
|
+
return 1;
|
|
48
|
+
} else {
|
|
49
|
+
console.log(chalk.yellow.bold(`Analysis complete: ${warningCount} warnings found.`));
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/scanner.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function scanCode(files, rules) {
|
|
2
|
+
const issues = [];
|
|
3
|
+
|
|
4
|
+
for (const file of files) {
|
|
5
|
+
for (const change of file.changes) {
|
|
6
|
+
for (const rule of rules) {
|
|
7
|
+
if (rule.regex.test(change.content)) {
|
|
8
|
+
issues.push({
|
|
9
|
+
file: file.path,
|
|
10
|
+
line: change.line,
|
|
11
|
+
content: change.content.trim(),
|
|
12
|
+
ruleId: rule.id,
|
|
13
|
+
message: rule.message,
|
|
14
|
+
severity: rule.severity
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return issues;
|
|
22
|
+
}
|