hackmenot 1.0.0__py3-none-any.whl

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.
Files changed (147) hide show
  1. hackmenot/__init__.py +3 -0
  2. hackmenot/__main__.py +6 -0
  3. hackmenot/cli/__init__.py +5 -0
  4. hackmenot/cli/git.py +68 -0
  5. hackmenot/cli/interactive.py +239 -0
  6. hackmenot/cli/main.py +454 -0
  7. hackmenot/core/__init__.py +30 -0
  8. hackmenot/core/cache.py +167 -0
  9. hackmenot/core/config.py +155 -0
  10. hackmenot/core/ignores.py +115 -0
  11. hackmenot/core/models.py +92 -0
  12. hackmenot/core/regex_cache.py +54 -0
  13. hackmenot/core/scanner.py +291 -0
  14. hackmenot/data/__init__.py +14 -0
  15. hackmenot/data/npm_top50k.txt +97 -0
  16. hackmenot/data/pypi_top50k.txt +104 -0
  17. hackmenot/deps/__init__.py +6 -0
  18. hackmenot/deps/hallucination.py +60 -0
  19. hackmenot/deps/parser.py +103 -0
  20. hackmenot/deps/scanner.py +48 -0
  21. hackmenot/deps/typosquat.py +90 -0
  22. hackmenot/deps/vulns.py +111 -0
  23. hackmenot/fixes/__init__.py +5 -0
  24. hackmenot/fixes/diff.py +104 -0
  25. hackmenot/fixes/engine.py +182 -0
  26. hackmenot/fixes/patterns.py +87 -0
  27. hackmenot/parsers/__init__.py +33 -0
  28. hackmenot/parsers/base.py +79 -0
  29. hackmenot/parsers/golang.py +343 -0
  30. hackmenot/parsers/javascript.py +347 -0
  31. hackmenot/parsers/python.py +150 -0
  32. hackmenot/parsers/terraform.py +211 -0
  33. hackmenot/reporters/__init__.py +7 -0
  34. hackmenot/reporters/base.py +14 -0
  35. hackmenot/reporters/markdown.py +53 -0
  36. hackmenot/reporters/sarif.py +84 -0
  37. hackmenot/reporters/terminal.py +174 -0
  38. hackmenot/rules/__init__.py +6 -0
  39. hackmenot/rules/builtin/auth/AUTH001.yml +27 -0
  40. hackmenot/rules/builtin/auth/AUTH002.yml +27 -0
  41. hackmenot/rules/builtin/auth/AUTH003.yml +29 -0
  42. hackmenot/rules/builtin/auth/AUTH004.yml +29 -0
  43. hackmenot/rules/builtin/auth/AUTH005.yml +28 -0
  44. hackmenot/rules/builtin/auth/AUTH006.yml +32 -0
  45. hackmenot/rules/builtin/auth/AUTH007.yml +33 -0
  46. hackmenot/rules/builtin/auth/JSAU001.yml +25 -0
  47. hackmenot/rules/builtin/auth/JSAU002.yml +31 -0
  48. hackmenot/rules/builtin/auth/JSAU003.yml +36 -0
  49. hackmenot/rules/builtin/auth/JSAU004.yml +33 -0
  50. hackmenot/rules/builtin/crypto/CRYPTO001.yml +29 -0
  51. hackmenot/rules/builtin/crypto/CRYPTO002.yml +28 -0
  52. hackmenot/rules/builtin/crypto/CRYPTO003.yml +34 -0
  53. hackmenot/rules/builtin/crypto/CRYPTO004.yml +34 -0
  54. hackmenot/rules/builtin/crypto/CRYPTO005.yml +35 -0
  55. hackmenot/rules/builtin/crypto/CRYPTO006.yml +35 -0
  56. hackmenot/rules/builtin/crypto/CRYPTO007.yml +36 -0
  57. hackmenot/rules/builtin/crypto/JSCR001.yml +25 -0
  58. hackmenot/rules/builtin/crypto/JSCR002.yml +29 -0
  59. hackmenot/rules/builtin/crypto/JSCR003.yml +32 -0
  60. hackmenot/rules/builtin/deps/DEP001.yml +27 -0
  61. hackmenot/rules/builtin/deps/DEP002.yml +26 -0
  62. hackmenot/rules/builtin/exposure/EXP001.yml +26 -0
  63. hackmenot/rules/builtin/exposure/EXP002.yml +27 -0
  64. hackmenot/rules/builtin/exposure/EXP003.yml +30 -0
  65. hackmenot/rules/builtin/exposure/EXP004.yml +35 -0
  66. hackmenot/rules/builtin/exposure/EXP005.yml +32 -0
  67. hackmenot/rules/builtin/exposure/EXP006.yml +32 -0
  68. hackmenot/rules/builtin/exposure/EXP007.yml +36 -0
  69. hackmenot/rules/builtin/go/GO_AUT001.yml +20 -0
  70. hackmenot/rules/builtin/go/GO_AUT002.yml +20 -0
  71. hackmenot/rules/builtin/go/GO_AUT003.yml +20 -0
  72. hackmenot/rules/builtin/go/GO_AUT004.yml +20 -0
  73. hackmenot/rules/builtin/go/GO_CON001.yml +22 -0
  74. hackmenot/rules/builtin/go/GO_CON002.yml +23 -0
  75. hackmenot/rules/builtin/go/GO_CON003.yml +22 -0
  76. hackmenot/rules/builtin/go/GO_CRY001.yml +28 -0
  77. hackmenot/rules/builtin/go/GO_CRY002.yml +28 -0
  78. hackmenot/rules/builtin/go/GO_CRY003.yml +28 -0
  79. hackmenot/rules/builtin/go/GO_CRY004.yml +28 -0
  80. hackmenot/rules/builtin/go/GO_CRY005.yml +30 -0
  81. hackmenot/rules/builtin/go/GO_INJ001.yml +34 -0
  82. hackmenot/rules/builtin/go/GO_INJ002.yml +29 -0
  83. hackmenot/rules/builtin/go/GO_INJ003.yml +32 -0
  84. hackmenot/rules/builtin/go/GO_INJ004.yml +28 -0
  85. hackmenot/rules/builtin/go/GO_INJ005.yml +29 -0
  86. hackmenot/rules/builtin/go/GO_INJ006.yml +29 -0
  87. hackmenot/rules/builtin/go/GO_NET001.yml +29 -0
  88. hackmenot/rules/builtin/go/GO_NET002.yml +23 -0
  89. hackmenot/rules/builtin/go/GO_NET003.yml +25 -0
  90. hackmenot/rules/builtin/go/GO_UNS001.yml +21 -0
  91. hackmenot/rules/builtin/go/GO_UNS002.yml +21 -0
  92. hackmenot/rules/builtin/injection/INJ001.yml +26 -0
  93. hackmenot/rules/builtin/injection/INJ002.yml +25 -0
  94. hackmenot/rules/builtin/injection/INJ003.yml +31 -0
  95. hackmenot/rules/builtin/injection/INJ004.yml +31 -0
  96. hackmenot/rules/builtin/injection/INJ005.yml +30 -0
  97. hackmenot/rules/builtin/injection/INJ006.yml +32 -0
  98. hackmenot/rules/builtin/injection/INJ007.yml +33 -0
  99. hackmenot/rules/builtin/injection/JSIJ001.yml +24 -0
  100. hackmenot/rules/builtin/injection/JSIJ002.yml +27 -0
  101. hackmenot/rules/builtin/injection/JSIJ003.yml +30 -0
  102. hackmenot/rules/builtin/injection/JSIJ004.yml +30 -0
  103. hackmenot/rules/builtin/injection/JSIJ005.yml +29 -0
  104. hackmenot/rules/builtin/terraform/TF_ENC001.yml +18 -0
  105. hackmenot/rules/builtin/terraform/TF_ENC002.yml +17 -0
  106. hackmenot/rules/builtin/terraform/TF_ENC003.yml +17 -0
  107. hackmenot/rules/builtin/terraform/TF_ENC004.yml +16 -0
  108. hackmenot/rules/builtin/terraform/TF_IAM001.yml +18 -0
  109. hackmenot/rules/builtin/terraform/TF_IAM002.yml +18 -0
  110. hackmenot/rules/builtin/terraform/TF_IAM003.yml +18 -0
  111. hackmenot/rules/builtin/terraform/TF_LOG001.yml +18 -0
  112. hackmenot/rules/builtin/terraform/TF_LOG002.yml +17 -0
  113. hackmenot/rules/builtin/terraform/TF_LOG003.yml +17 -0
  114. hackmenot/rules/builtin/terraform/TF_NET001.yml +18 -0
  115. hackmenot/rules/builtin/terraform/TF_NET002.yml +17 -0
  116. hackmenot/rules/builtin/terraform/TF_S3001.yml +21 -0
  117. hackmenot/rules/builtin/terraform/TF_S3002.yml +17 -0
  118. hackmenot/rules/builtin/terraform/TF_S3003.yml +17 -0
  119. hackmenot/rules/builtin/terraform/TF_S3004.yml +17 -0
  120. hackmenot/rules/builtin/terraform/TF_SEC001.yml +17 -0
  121. hackmenot/rules/builtin/terraform/TF_SEC002.yml +17 -0
  122. hackmenot/rules/builtin/terraform/TF_SEC003.yml +17 -0
  123. hackmenot/rules/builtin/terraform/TF_SEC004.yml +17 -0
  124. hackmenot/rules/builtin/terraform/TF_SG001.yml +18 -0
  125. hackmenot/rules/builtin/terraform/TF_SG002.yml +18 -0
  126. hackmenot/rules/builtin/terraform/TF_SG003.yml +18 -0
  127. hackmenot/rules/builtin/terraform/TF_SG004.yml +18 -0
  128. hackmenot/rules/builtin/validation/JSVA001.yml +26 -0
  129. hackmenot/rules/builtin/validation/JSVA002.yml +32 -0
  130. hackmenot/rules/builtin/validation/JSVA003.yml +34 -0
  131. hackmenot/rules/builtin/validation/JSVA004.yml +38 -0
  132. hackmenot/rules/builtin/validation/VAL001.yml +29 -0
  133. hackmenot/rules/builtin/validation/VAL002.yml +32 -0
  134. hackmenot/rules/builtin/validation/VAL003.yml +31 -0
  135. hackmenot/rules/builtin/validation/VAL004.yml +34 -0
  136. hackmenot/rules/builtin/validation/VAL005.yml +40 -0
  137. hackmenot/rules/builtin/xss/XSS001.yml +26 -0
  138. hackmenot/rules/builtin/xss/XSS002.yml +29 -0
  139. hackmenot/rules/builtin/xss/XSS003.yml +29 -0
  140. hackmenot/rules/builtin/xss/XSS004.yml +32 -0
  141. hackmenot/rules/engine.py +489 -0
  142. hackmenot/rules/registry.py +100 -0
  143. hackmenot-1.0.0.dist-info/METADATA +235 -0
  144. hackmenot-1.0.0.dist-info/RECORD +147 -0
  145. hackmenot-1.0.0.dist-info/WHEEL +4 -0
  146. hackmenot-1.0.0.dist-info/entry_points.txt +2 -0
  147. hackmenot-1.0.0.dist-info/licenses/LICENSE +201 -0
hackmenot/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """hackmenot - AI-Era Code Security Scanner."""
2
+
3
+ __version__ = "1.0.0"
hackmenot/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for python -m hackmenot."""
2
+
3
+ from hackmenot.cli.main import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1,5 @@
1
+ """CLI module for hackmenot."""
2
+
3
+ from hackmenot.cli.main import app
4
+
5
+ __all__ = ["app"]
hackmenot/cli/git.py ADDED
@@ -0,0 +1,68 @@
1
+ """Git helpers for CI integration."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+
7
+ def get_staged_files() -> list[Path]:
8
+ """Get list of staged files from git.
9
+
10
+ Returns files that are:
11
+ - A: Added
12
+ - C: Copied
13
+ - M: Modified
14
+ - R: Renamed
15
+
16
+ Returns:
17
+ List of Path objects for staged files.
18
+ """
19
+ try:
20
+ result = subprocess.run(
21
+ ["git", "diff", "--cached", "--name-only", "--diff-filter=ACMR"],
22
+ capture_output=True,
23
+ text=True,
24
+ check=True,
25
+ )
26
+ files = [Path(f) for f in result.stdout.strip().split("\n") if f]
27
+ return files
28
+ except subprocess.CalledProcessError:
29
+ return []
30
+
31
+
32
+ def is_git_repo() -> bool:
33
+ """Check if current directory is a git repository.
34
+
35
+ Returns:
36
+ True if in a git repository, False otherwise.
37
+ """
38
+ try:
39
+ subprocess.run(
40
+ ["git", "rev-parse", "--git-dir"],
41
+ capture_output=True,
42
+ check=True,
43
+ )
44
+ return True
45
+ except subprocess.CalledProcessError:
46
+ return False
47
+
48
+
49
+ def get_changed_files(since: str) -> list[Path]:
50
+ """Get list of files changed since a specific commit/ref.
51
+
52
+ Args:
53
+ since: A git ref (commit SHA, branch name, tag, etc.)
54
+
55
+ Returns:
56
+ List of Path objects for changed files.
57
+ """
58
+ try:
59
+ result = subprocess.run(
60
+ ["git", "diff", "--name-only", since, "HEAD"],
61
+ capture_output=True,
62
+ text=True,
63
+ check=True,
64
+ )
65
+ files = [Path(f) for f in result.stdout.strip().split("\n") if f]
66
+ return files
67
+ except subprocess.CalledProcessError:
68
+ return []
@@ -0,0 +1,239 @@
1
+ """Interactive fix mode for CLI."""
2
+
3
+ from pathlib import Path
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.prompt import Prompt
8
+ from rich.syntax import Syntax
9
+
10
+ from hackmenot.core.models import Finding
11
+ from hackmenot.fixes.engine import FixEngine
12
+ from hackmenot.rules.registry import RuleRegistry
13
+
14
+
15
+ class InteractiveFixer:
16
+ """Interactive fixer that prompts user for each finding."""
17
+
18
+ def __init__(self, console: Console | None = None):
19
+ """Initialize the interactive fixer.
20
+
21
+ Args:
22
+ console: Rich console for output. Creates new one if not provided.
23
+ """
24
+ self.console = console or Console()
25
+ self.engine = FixEngine()
26
+ # Load rules for pattern-based fixes
27
+ registry = RuleRegistry()
28
+ registry.load_all()
29
+ self.rules = {rule.id: rule for rule in registry.get_all_rules()}
30
+
31
+ def run(
32
+ self, findings: list[Finding], file_contents: dict[str, str]
33
+ ) -> dict[str, str]:
34
+ """Run interactive fix mode.
35
+
36
+ Shows each finding with its fix suggestion and prompts the user
37
+ for action: [a]pply, [s]kip, [A]pply all, [q]uit.
38
+
39
+ Args:
40
+ findings: List of findings to potentially fix.
41
+ file_contents: Dictionary mapping file paths to their contents.
42
+
43
+ Returns:
44
+ Dictionary mapping file paths to their (potentially modified) contents.
45
+ """
46
+ result = dict(file_contents)
47
+ apply_all = False
48
+
49
+ # Group findings by file and sort by line number descending
50
+ # (so we can apply fixes without line number shifts)
51
+ findings_by_file: dict[str, list[Finding]] = {}
52
+ for finding in findings:
53
+ if finding.fix_suggestion:
54
+ if finding.file_path not in findings_by_file:
55
+ findings_by_file[finding.file_path] = []
56
+ findings_by_file[finding.file_path].append(finding)
57
+
58
+ # Sort each file's findings by line number descending
59
+ for file_path in findings_by_file:
60
+ findings_by_file[file_path].sort(
61
+ key=lambda f: f.line_number, reverse=True
62
+ )
63
+
64
+ total_findings = sum(len(f) for f in findings_by_file.values())
65
+ current = 0
66
+
67
+ for file_path, file_findings in findings_by_file.items():
68
+ for finding in file_findings:
69
+ current += 1
70
+
71
+ if apply_all:
72
+ # Apply without prompting
73
+ self._apply_fix(finding, result)
74
+ continue
75
+
76
+ # Show finding details
77
+ self._show_finding(finding, current, total_findings)
78
+
79
+ # Prompt for action
80
+ action = Prompt.ask(
81
+ "[bold cyan]Action[/bold cyan]",
82
+ choices=["a", "s", "A", "q"],
83
+ default="s",
84
+ )
85
+
86
+ if action == "q":
87
+ self.console.print("[yellow]Quit - stopping fix mode[/yellow]")
88
+ return result
89
+ elif action == "A":
90
+ self.console.print(
91
+ "[green]Applying all remaining fixes...[/green]"
92
+ )
93
+ apply_all = True
94
+ self._apply_fix(finding, result)
95
+ elif action == "a":
96
+ self._apply_fix(finding, result)
97
+ self.console.print("[green]Fix applied[/green]")
98
+ else: # "s"
99
+ self.console.print("[dim]Skipped[/dim]")
100
+
101
+ self.console.print("\n[bold green]Done![/bold green] Interactive fix mode complete.")
102
+ return result
103
+
104
+ def _show_finding(
105
+ self, finding: Finding, current: int, total: int
106
+ ) -> None:
107
+ """Display a finding with its fix suggestion.
108
+
109
+ Args:
110
+ finding: The finding to display.
111
+ current: Current finding number.
112
+ total: Total number of findings.
113
+ """
114
+ self.console.print()
115
+ self.console.print(
116
+ f"[bold]Finding {current}/{total}[/bold] - "
117
+ f"[cyan]{finding.rule_id}[/cyan]: {finding.rule_name}"
118
+ )
119
+ self.console.print(
120
+ f"[dim]{finding.file_path}:{finding.line_number}[/dim]"
121
+ )
122
+ self.console.print()
123
+
124
+ # Show the problematic code
125
+ self.console.print("[bold red]Current code:[/bold red]")
126
+ syntax = Syntax(
127
+ finding.code_snippet,
128
+ "python",
129
+ theme="monokai",
130
+ line_numbers=False,
131
+ )
132
+ self.console.print(syntax)
133
+ self.console.print()
134
+
135
+ # Show the fix suggestion
136
+ self.console.print("[bold green]Suggested fix:[/bold green]")
137
+ fix_syntax = Syntax(
138
+ finding.fix_suggestion,
139
+ "python",
140
+ theme="monokai",
141
+ line_numbers=False,
142
+ )
143
+ self.console.print(fix_syntax)
144
+ self.console.print()
145
+
146
+ self.console.print(
147
+ "[dim]\\[a]pply \\[s]kip \\[A]pply all \\[q]uit[/dim]"
148
+ )
149
+
150
+ def _apply_fix(
151
+ self, finding: Finding, file_contents: dict[str, str]
152
+ ) -> None:
153
+ """Apply a fix to the file contents.
154
+
155
+ Args:
156
+ finding: The finding with the fix to apply.
157
+ file_contents: Dictionary of file contents to modify in place.
158
+ """
159
+ if finding.file_path in file_contents:
160
+ source = file_contents[finding.file_path]
161
+ rule = self.rules.get(finding.rule_id)
162
+ result = self.engine.apply_fix(source, finding, rule)
163
+ if result.applied:
164
+ # Rebuild source with the fix applied
165
+ lines = source.split("\n")
166
+ line_idx = finding.line_number - 1
167
+ if 0 <= line_idx < len(lines):
168
+ fixed_lines = result.fixed.split("\n")
169
+ lines[line_idx : line_idx + 1] = fixed_lines
170
+ file_contents[finding.file_path] = "\n".join(lines)
171
+
172
+
173
+ def apply_fixes_auto(
174
+ findings: list[Finding],
175
+ file_contents: dict[str, str],
176
+ console: Console | None = None,
177
+ ) -> tuple[dict[str, str], int]:
178
+ """Apply all fixes automatically.
179
+
180
+ Args:
181
+ findings: List of findings to fix.
182
+ file_contents: Dictionary mapping file paths to their contents.
183
+ console: Rich console for output.
184
+
185
+ Returns:
186
+ Tuple of (modified file contents dict, number of fixes applied).
187
+ """
188
+ console = console or Console()
189
+ engine = FixEngine()
190
+ result = dict(file_contents)
191
+ total_applied = 0
192
+
193
+ # Load rules for pattern-based fixes
194
+ registry = RuleRegistry()
195
+ registry.load_all()
196
+ rules = {rule.id: rule for rule in registry.get_all_rules()}
197
+
198
+ # Group findings by file
199
+ findings_by_file: dict[str, list[Finding]] = {}
200
+ for finding in findings:
201
+ if finding.fix_suggestion:
202
+ if finding.file_path not in findings_by_file:
203
+ findings_by_file[finding.file_path] = []
204
+ findings_by_file[finding.file_path].append(finding)
205
+
206
+ for file_path, file_findings in findings_by_file.items():
207
+ if file_path in result:
208
+ fixed, count = engine.apply_fixes(result[file_path], file_findings, rules)
209
+ result[file_path] = fixed
210
+ total_applied += count
211
+
212
+ if total_applied > 0:
213
+ console.print(f"[green]Applied {total_applied} fix(es)[/green]")
214
+ else:
215
+ console.print("[yellow]No fixes to apply[/yellow]")
216
+
217
+ return result, total_applied
218
+
219
+
220
+ def write_fixed_files(
221
+ file_contents: dict[str, str], original_contents: dict[str, str]
222
+ ) -> int:
223
+ """Write modified files back to disk.
224
+
225
+ Only writes files that have been modified.
226
+
227
+ Args:
228
+ file_contents: Dictionary of (potentially) modified file contents.
229
+ original_contents: Dictionary of original file contents.
230
+
231
+ Returns:
232
+ Number of files written.
233
+ """
234
+ written = 0
235
+ for file_path, content in file_contents.items():
236
+ if content != original_contents.get(file_path):
237
+ Path(file_path).write_text(content)
238
+ written += 1
239
+ return written