push-sentinel 0.1.0 → 0.1.2

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 CHANGED
@@ -1,88 +1,79 @@
1
1
  # push-sentinel
2
2
 
3
- Warns you if secrets are in your git commits before push.
3
+ **Catches secrets in your git commits before they leave your machine.**
4
4
 
5
- ## What it does
5
+ You've seen the stories. Someone pushes an AWS key to a public repo. Bots scrape GitHub in seconds. The bill arrives the next morning: $8,000.
6
6
 
7
- Scans the commits you are about to push for potential secrets (API keys, private keys, tokens) and prints a warning. **It does not block the push by default** — it is a safety net, not a gatekeeper.
7
+ push-sentinel sits in your `pre-push` hook and warns you before that happens.
8
8
 
9
- ## Install
10
-
11
- ```sh
12
- npx push-sentinel install
13
9
  ```
10
+ $ git push
14
11
 
15
- This writes a `pre-push` hook to `.git/hooks/`. Any existing hook is preserved as `pre-push.local` and called automatically after the scan.
16
-
17
- ## Usage
18
-
19
- ### Automatic (after install)
20
-
21
- The hook runs on every `git push`. No action required.
22
-
23
- ```
24
12
  [push-sentinel] ⚠ Potential secrets found:
25
13
 
26
14
  [HIGH] src/config.ts:12
27
15
  AKIAIO...
28
- → Risk: Full access to AWS resources. Attacker can create/delete instances, incur charges, or exfiltrate data.
16
+ → Risk: Full access to AWS resources. Attacker can create/delete
17
+ instances, incur charges, or exfiltrate data.
29
18
  → To ignore this line: push-sentinel ignore src/config.ts:12
30
19
 
31
20
  Push continues. Double-check before sharing.
32
21
  ```
33
22
 
34
- ### Manual scan
23
+ ## Install
35
24
 
36
25
  ```sh
37
- npx push-sentinel scan
26
+ npx push-sentinel install
38
27
  ```
39
28
 
40
- ### Block push on HIGH findings
29
+ That's it. Runs automatically on every `git push` from now on.
41
30
 
42
- To treat HIGH severity findings as blocking errors, edit `.git/hooks/pre-push` and change the scan line to:
31
+ ## What it detects
43
32
 
44
- ```sh
45
- npx push-sentinel scan --local-sha "$local_sha" --remote-sha "$remote_sha" --block-on-high
46
- ```
33
+ | Pattern | Severity |
34
+ |---------|----------|
35
+ | Private Key (RSA, EC, OPENSSH, DSA, PKCS#8) | 🔴 HIGH |
36
+ | AWS Access Key (`AKIA...`) | 🔴 HIGH |
37
+ | AWS Secret Key (entropy-based) | 🔴 HIGH |
38
+ | GitHub Token (`ghp_`, `github_pat_`) | 🔴 HIGH |
39
+ | Anthropic API Key (`sk-ant-...`) | 🟡 MEDIUM |
40
+ | OpenAI API Key (`sk-...`) | 🟡 MEDIUM |
41
+ | Generic API Key (variable name + high entropy) | 🟢 LOW |
42
+ | `.env` file committed | 🟡 MEDIUM |
47
43
 
48
- ## Suppressing false positives
44
+ ## False positive? Ignore it in one command
49
45
 
50
46
  ```sh
51
- # Ignore a specific line
52
- push-sentinel ignore src/config.ts:12
47
+ push-sentinel ignore src/config.ts:12 # ignore a specific line
48
+ push-sentinel ignore --pattern OPENAI_API_KEY # ignore a pattern everywhere
49
+ push-sentinel ignore --list # see all ignore rules
50
+ ```
53
51
 
54
- # Ignore all matches of a pattern name
55
- push-sentinel ignore --pattern OPENAI_API_KEY
52
+ Rules are saved to `.push-sentinel-ignore` in your repo root.
56
53
 
57
- # List current ignore rules
58
- push-sentinel ignore --list
54
+ ## Why warning-only by default?
59
55
 
60
- # Remove a rule
61
- push-sentinel ignore --remove OPENAI_API_KEY
62
- ```
56
+ Blocking pushes creates friction. Friction leads to `--no-verify`. A warning at push time is early enough to catch real accidents — and you'll actually leave it installed.
63
57
 
64
- Rules are saved to `.push-sentinel-ignore` in the repo root. Add it to `.gitignore` or commit it — your choice.
58
+ Want hard blocking for HIGH findings? Add `--block-on-high`:
65
59
 
66
- ## Detected patterns
60
+ ```sh
61
+ # edit .git/hooks/pre-push, change the scan line to:
62
+ npx push-sentinel scan --local-sha "$local_sha" --remote-sha "$remote_sha" --block-on-high
63
+ ```
67
64
 
68
- | Pattern | Severity |
69
- |---------|----------|
70
- | Private Key (RSA, EC, OPENSSH, DSA, PKCS#8) | HIGH |
71
- | AWS Access Key (`AKIA...`) | HIGH |
72
- | AWS Secret Key (entropy-based) | HIGH |
73
- | GitHub Token (`ghp_`, `github_pat_`) | HIGH |
74
- | Anthropic API Key (`sk-ant-...`) | MEDIUM |
75
- | OpenAI API Key (`sk-...`) | MEDIUM |
76
- | Generic API Key (variable name + high entropy) | LOW |
77
- | `.env` file committed | MEDIUM |
65
+ ## Manual scan
78
66
 
79
- ## Non-blocking design
67
+ ```sh
68
+ npx push-sentinel scan
69
+ ```
80
70
 
81
- push-sentinel warns it does not block — because:
71
+ Manual scan checks, in order:
82
72
 
83
- - Blocking creates friction that causes developers to skip or uninstall the hook
84
- - A warning seen at push time is still early enough to catch accidental leaks
85
- - Use `--block-on-high` if you want stricter enforcement for HIGH-severity findings
73
+ - commits not yet pushed to your upstream
74
+ - staged changes
75
+ - unstaged working tree changes
76
+ - the last commit as a final fallback
86
77
 
87
78
  ## Uninstall
88
79
 
@@ -90,9 +81,11 @@ push-sentinel warns — it does not block — because:
90
81
  npx push-sentinel uninstall
91
82
  ```
92
83
 
93
- The original `pre-push` hook (if any) is automatically restored.
84
+ Your original `pre-push` hook is restored automatically.
94
85
 
95
- ## Requirements
86
+ ## Details
96
87
 
88
+ - Scans only the commits being pushed — not your entire history
89
+ - Zero dependencies (Node.js stdlib only)
97
90
  - Node.js >= 16
98
- - No additional dependencies
91
+ - Existing `pre-push` hooks are preserved and still run
package/bin/cli.js CHANGED
@@ -100,8 +100,9 @@ switch (command) {
100
100
  const localSha = localShaIdx !== -1 ? args[localShaIdx + 1] : undefined;
101
101
  const remoteSha = remoteShaIdx !== -1 ? args[remoteShaIdx + 1] : undefined;
102
102
  const findings = scan(localSha, remoteSha);
103
- report(findings);
104
- if (blockOnHigh && hasHighFindings(findings)) {
103
+ const willBlock = blockOnHigh && hasHighFindings(findings);
104
+ report(findings, { willBlock });
105
+ if (willBlock) {
105
106
  process.stderr.write('\n[push-sentinel] Push blocked: HIGH severity secret(s) detected. Remove the secret or run `push-sentinel ignore` to suppress.\n');
106
107
  process.exit(1);
107
108
  }
package/hook-template.sh CHANGED
@@ -6,10 +6,14 @@
6
6
  #
7
7
  # We read each line and pass the SHAs explicitly so push-sentinel can compute
8
8
  # the exact range of commits being pushed (including new branch / first push).
9
+ # Stdin is saved and re-supplied to pre-push.local so existing hooks still work.
9
10
 
10
11
  EXIT_CODE=0
12
+ STDIN_DATA=""
11
13
 
12
14
  while read local_ref local_sha remote_ref remote_sha; do
15
+ STDIN_DATA="${STDIN_DATA}${local_ref} ${local_sha} ${remote_ref} ${remote_sha}
16
+ "
13
17
  npx push-sentinel scan --local-sha "$local_sha" --remote-sha "$remote_sha"
14
18
  RESULT=$?
15
19
  if [ $RESULT -ne 0 ]; then
@@ -22,5 +26,5 @@ if [ $EXIT_CODE -ne 0 ]; then
22
26
  fi
23
27
 
24
28
  if [ -f "$(git rev-parse --git-dir)/hooks/pre-push.local" ]; then
25
- "$(git rev-parse --git-dir)/hooks/pre-push.local" "$@"
29
+ echo "$STDIN_DATA" | "$(git rev-parse --git-dir)/hooks/pre-push.local" "$@"
26
30
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "push-sentinel",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Warns you if secrets are in your git diff before push.",
5
5
  "bin": {
6
6
  "push-sentinel": "bin/cli.js"
package/src/install.js CHANGED
@@ -33,10 +33,14 @@ ${SENTINEL_MARKER}
33
33
  # <local-ref> <local-sha1> <remote-ref> <remote-sha1>
34
34
  # We forward the SHAs so push-sentinel can determine exactly which commits
35
35
  # are being pushed, including new branches (remote-sha = 0000...0000).
36
+ # Stdin is saved and re-supplied to pre-push.local so existing hooks still work.
36
37
 
37
38
  EXIT_CODE=0
39
+ STDIN_DATA=""
38
40
 
39
41
  while read local_ref local_sha remote_ref remote_sha; do
42
+ STDIN_DATA="${'$'}{STDIN_DATA}${'$'}{local_ref} ${'$'}{local_sha} ${'$'}{remote_ref} ${'$'}{remote_sha}
43
+ "
40
44
  npx push-sentinel scan --local-sha "$local_sha" --remote-sha "$remote_sha"
41
45
  RESULT=$?
42
46
  if [ $RESULT -ne 0 ]; then
@@ -49,7 +53,7 @@ if [ $EXIT_CODE -ne 0 ]; then
49
53
  fi
50
54
 
51
55
  if [ -f "$(git rev-parse --git-dir)/hooks/pre-push.local" ]; then
52
- "$(git rev-parse --git-dir)/hooks/pre-push.local" "$@"
56
+ echo "$STDIN_DATA" | "$(git rev-parse --git-dir)/hooks/pre-push.local" "$@"
53
57
  fi
54
58
  `;
55
59
  }
package/src/reporter.js CHANGED
@@ -13,7 +13,7 @@ function severityLabel(severity) {
13
13
  return `[${severity}]`;
14
14
  }
15
15
 
16
- function report(findings) {
16
+ function report(findings, { willBlock = false } = {}) {
17
17
  if (findings.length === 0) {
18
18
  process.stderr.write('[push-sentinel] \u2713 No secrets detected.\n');
19
19
  return;
@@ -39,7 +39,11 @@ function report(findings) {
39
39
  process.stderr.write(` + ${extra} more finding(s) not shown.\n\n`);
40
40
  }
41
41
 
42
- process.stderr.write(' Push continues. Double-check before sharing.\n');
42
+ if (willBlock) {
43
+ process.stderr.write(' Push will be blocked.\n');
44
+ } else {
45
+ process.stderr.write(' Push continues. Double-check before sharing.\n');
46
+ }
43
47
  }
44
48
 
45
49
  function hasHighFindings(findings) {
package/src/scan.js CHANGED
@@ -55,33 +55,31 @@ function loadIgnoreRules(repoRoot) {
55
55
  return { lines, patterns };
56
56
  }
57
57
 
58
+ // Check if a file path matches a single glob-style ignore rule
59
+ function matchesGlob(filePath, rule) {
60
+ if (rule.endsWith('/**')) {
61
+ const dir = rule.slice(0, -3);
62
+ return filePath.startsWith(dir + '/') || filePath === dir;
63
+ }
64
+ if (rule.endsWith('*')) {
65
+ return filePath.startsWith(rule.slice(0, -1));
66
+ }
67
+ return false;
68
+ }
69
+
58
70
  // Check if a file (regardless of line) matches any ignore rule
59
71
  function matchesIgnoreFile(filePath, ignoreLines) {
60
72
  for (const rule of ignoreLines) {
61
- if (rule === filePath) return true;
62
- if (rule.endsWith('/**')) {
63
- const dir = rule.slice(0, -3);
64
- if (filePath.startsWith(dir + '/') || filePath === dir) return true;
65
- }
66
- if (rule.endsWith('*')) {
67
- if (filePath.startsWith(rule.slice(0, -1))) return true;
68
- }
73
+ if (rule === filePath || matchesGlob(filePath, rule)) return true;
69
74
  }
70
75
  return false;
71
76
  }
72
77
 
73
- // Check if a file:line matches any ignore rule (simple glob support)
78
+ // Check if a file:line matches any ignore rule
74
79
  function matchesIgnoreLine(filePath, lineNum, ignoreLines) {
75
80
  const target = `${filePath}:${lineNum}`;
76
81
  for (const rule of ignoreLines) {
77
- if (rule === target) return true;
78
- if (rule.endsWith('/**')) {
79
- const dir = rule.slice(0, -3);
80
- if (filePath.startsWith(dir + '/') || filePath === dir) return true;
81
- }
82
- if (rule.endsWith('*')) {
83
- if (filePath.startsWith(rule.slice(0, -1))) return true;
84
- }
82
+ if (rule === target || matchesGlob(filePath, rule)) return true;
85
83
  }
86
84
  return false;
87
85
  }
@@ -121,14 +119,24 @@ function runGit(args) {
121
119
  return result.status === 0 ? (result.stdout || '') : null;
122
120
  }
123
121
 
122
+ function runGitNonEmpty(args) {
123
+ const out = runGit(args);
124
+ return out && out.trim() ? out : null;
125
+ }
126
+
124
127
  function getPushDiff(localSha, remoteSha) {
125
128
  const args = getDiffArgs(localSha, remoteSha);
126
129
  if (args) {
127
130
  const out = runGit(args);
128
131
  if (out !== null) return out;
129
132
  }
130
- // Fallback for manual scan
131
- return runGit(['log', '@{u}..HEAD', '-p']) ?? runGit(['log', '-1', '-p', 'HEAD']) ?? '';
133
+ // Manual scan: inspect unpushed commits first, then staged changes, then working tree,
134
+ // and finally fall back to the last commit if none of those produce a diff.
135
+ return runGitNonEmpty(['log', '@{u}..HEAD', '-p'])
136
+ ?? runGitNonEmpty(['diff', '--cached'])
137
+ ?? runGitNonEmpty(['diff'])
138
+ ?? runGit(['log', '-1', '-p', 'HEAD'])
139
+ ?? '';
132
140
  }
133
141
 
134
142
  function getPushedFileList(localSha, remoteSha) {
@@ -137,8 +145,10 @@ function getPushedFileList(localSha, remoteSha) {
137
145
  const out = runGit(args);
138
146
  if (out !== null) return out.split('\n').map((f) => f.trim()).filter(Boolean);
139
147
  }
140
- // Fallback for manual scan
141
- const out = runGit(['diff', '--name-only', '@{u}..HEAD'])
148
+ // Manual scan: include files from unpushed commits, staged changes, and working tree.
149
+ const out = runGitNonEmpty(['diff', '--name-only', '@{u}..HEAD'])
150
+ ?? runGitNonEmpty(['diff', '--name-only', '--cached'])
151
+ ?? runGitNonEmpty(['diff', '--name-only'])
142
152
  ?? runGit(['diff', '--name-only', 'HEAD~1..HEAD'])
143
153
  ?? '';
144
154
  return out.split('\n').map((f) => f.trim()).filter(Boolean);