codeguard-cli 2.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 (52) hide show
  1. codeguard/__init__.py +7 -0
  2. codeguard/cli/__init__.py +2 -0
  3. codeguard/cli/_run.py +230 -0
  4. codeguard/cli/commands.py +390 -0
  5. codeguard/cli/formatters.py +422 -0
  6. codeguard/cli/main.py +206 -0
  7. codeguard/config/__init__.py +16 -0
  8. codeguard/config/loader.py +86 -0
  9. codeguard/config/schema.py +172 -0
  10. codeguard/engine/__init__.py +25 -0
  11. codeguard/engine/baseline.py +122 -0
  12. codeguard/engine/context.py +61 -0
  13. codeguard/engine/discovery.py +160 -0
  14. codeguard/engine/finding.py +205 -0
  15. codeguard/engine/fingerprint.py +94 -0
  16. codeguard/engine/gitdiff.py +80 -0
  17. codeguard/engine/policy.py +74 -0
  18. codeguard/engine/registry.py +78 -0
  19. codeguard/engine/rule.py +195 -0
  20. codeguard/engine/runner.py +267 -0
  21. codeguard/engine/suppressions.py +109 -0
  22. codeguard/lang/__init__.py +37 -0
  23. codeguard/lang/base.py +80 -0
  24. codeguard/lang/javascript.py +20 -0
  25. codeguard/lang/node.py +137 -0
  26. codeguard/lang/python_ast.py +29 -0
  27. codeguard/lang/registry.py +38 -0
  28. codeguard/lang/treesitter.py +99 -0
  29. codeguard/lang/typescript.py +24 -0
  30. codeguard/py.typed +1 -0
  31. codeguard/rules/__init__.py +6 -0
  32. codeguard/rules/_jsnodes.py +82 -0
  33. codeguard/rules/_pyimports.py +60 -0
  34. codeguard/rules/javascript/__init__.py +9 -0
  35. codeguard/rules/javascript/cg_sec_101_dynamic_code.py +89 -0
  36. codeguard/rules/javascript/cg_sec_102_child_process.py +58 -0
  37. codeguard/rules/javascript/cg_sec_103_dom_xss.py +67 -0
  38. codeguard/rules/javascript/cg_sec_104_react_dangerous_html.py +54 -0
  39. codeguard/rules/javascript/cg_sec_105_hardcoded_secret.py +73 -0
  40. codeguard/rules/javascript/cg_sec_106_weak_random.py +83 -0
  41. codeguard/rules/meta/__init__.py +55 -0
  42. codeguard/rules/security/__init__.py +8 -0
  43. codeguard/rules/security/cg_sec_001_sql_injection.py +110 -0
  44. codeguard/rules/security/cg_sec_002_hardcoded_secrets.py +184 -0
  45. codeguard/rules/security/cg_sec_003_eval_exec.py +104 -0
  46. codeguard/rules/security/cg_sec_004_unsafe_deserialization.py +156 -0
  47. codeguard/rules/security/cg_sec_005_shell_injection.py +157 -0
  48. codeguard_cli-2.0.0.dist-info/METADATA +210 -0
  49. codeguard_cli-2.0.0.dist-info/RECORD +52 -0
  50. codeguard_cli-2.0.0.dist-info/WHEEL +4 -0
  51. codeguard_cli-2.0.0.dist-info/entry_points.txt +2 -0
  52. codeguard_cli-2.0.0.dist-info/licenses/LICENSE +184 -0
@@ -0,0 +1,104 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """CG-SEC-003 — eval() / exec() called with a non-literal argument.
3
+
4
+ Detects calls to ``eval``, ``exec``, or ``compile`` where the first argument
5
+ is not a string literal — meaning the code being executed is dynamic and may
6
+ be attacker-controlled.
7
+
8
+ Why this matters
9
+ ----------------
10
+ ``eval()`` and ``exec()`` execute arbitrary Python. When called with
11
+ user-controlled or externally-sourced input they allow remote code execution
12
+ (CWE-78, CWE-95, OWASP A03:2021).
13
+
14
+ AI models frequently generate ``eval(user_input)`` or ``exec(command)``
15
+ patterns because they look like concise solutions to dynamic-dispatch problems.
16
+
17
+ False-positive guidance
18
+ -----------------------
19
+ ``eval("1 + 1")`` with a *literal* string is intentionally excluded — that is
20
+ a code smell but not the dangerous case. If your codebase legitimately calls
21
+ ``eval`` on trusted, internally-constructed strings, suppress with:
22
+ ``# codeguard: ignore[CG-SEC-003]``
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import ast
28
+
29
+ from codeguard.engine.finding import Category, Finding, Severity
30
+ from codeguard.engine.registry import REGISTRY
31
+ from codeguard.engine.rule import AstRule
32
+
33
+ _DANGEROUS_BUILTINS = frozenset({"eval", "exec", "compile"})
34
+
35
+ _FIX = (
36
+ "Avoid eval/exec on dynamic input. Use a safe dispatch mechanism "
37
+ "(a dict of callables, importlib, or ast.literal_eval for data parsing)."
38
+ )
39
+
40
+
41
+ class EvalExecRule(AstRule):
42
+ """Detect eval/exec/compile called with a non-literal argument."""
43
+
44
+ id = "CG-SEC-003"
45
+ title = "eval() / exec() on dynamic input"
46
+ description = (
47
+ "eval(), exec(), or compile() is called with a non-literal argument. "
48
+ "If the argument can be influenced by external input this is a remote "
49
+ "code execution vulnerability."
50
+ )
51
+ severity = Severity.HIGH
52
+ category = Category.SECURITY
53
+ cwe = "CWE-95"
54
+ owasp = "A03:2021 - Injection"
55
+
56
+ def check_ast(self, tree: ast.AST, source: str, filename: str) -> list[Finding]:
57
+ """Find eval/exec/compile calls with dynamic first arguments."""
58
+ findings: list[Finding] = []
59
+
60
+ for node in ast.walk(tree):
61
+ if not isinstance(node, ast.Call):
62
+ continue
63
+
64
+ func_name = self._func_name(node)
65
+ if func_name not in _DANGEROUS_BUILTINS:
66
+ continue
67
+
68
+ # No arguments at all — unusual but not our concern
69
+ if not node.args:
70
+ continue
71
+
72
+ first_arg = node.args[0]
73
+ # Safe case: literal string — eval("1+1") is a code smell, not RCE
74
+ if isinstance(first_arg, ast.Constant) and isinstance(first_arg.value, str):
75
+ continue
76
+
77
+ findings.append(
78
+ self._make_finding(
79
+ node=node,
80
+ filename=filename,
81
+ description=(
82
+ f"{func_name}() is called with a non-literal argument. "
83
+ "If this value is attacker-controlled it enables arbitrary "
84
+ "code execution."
85
+ ),
86
+ fix_suggestion=_FIX,
87
+ )
88
+ )
89
+
90
+ return findings
91
+
92
+ # ------------------------------------------------------------------
93
+ # Helpers
94
+ # ------------------------------------------------------------------
95
+
96
+ @staticmethod
97
+ def _func_name(node: ast.Call) -> str:
98
+ """Return the bare function name for simple Name calls, else ''."""
99
+ if isinstance(node.func, ast.Name):
100
+ return node.func.id
101
+ return ""
102
+
103
+
104
+ REGISTRY.register(EvalExecRule())
@@ -0,0 +1,156 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """CG-SEC-004 — Unsafe deserialization via pickle or yaml.load.
3
+
4
+ Detects:
5
+ - ``pickle.loads(...)`` / ``pickle.load(...)``
6
+ - ``_pickle.loads(...)`` (CPython internal alias)
7
+ - ``marshal.loads(...)``
8
+ - ``yaml.load(...)`` without ``Loader=SafeLoader`` (or ``Loader=yaml.SafeLoader``)
9
+
10
+ Why this matters
11
+ ----------------
12
+ Deserializing data from untrusted sources using pickle or unsafe yaml is a
13
+ critical vulnerability (CWE-502, OWASP A08:2021). Pickle can execute
14
+ arbitrary Python during deserialization; yaml.load() with the default
15
+ (FullLoader or older unsafe Loader) can instantiate arbitrary Python objects.
16
+
17
+ AI models commonly generate ``pickle.loads(data)`` and ``yaml.load(config)``
18
+ because the documentation examples often omit the Loader parameter.
19
+
20
+ False-positive guidance
21
+ -----------------------
22
+ ``pickle.loads`` on *internally-generated, trusted* data is still risky (any
23
+ attacker who can modify your data store gets RCE) but you can suppress if
24
+ you've audited the data provenance:
25
+ ``# codeguard: ignore[CG-SEC-004]``
26
+
27
+ ``yaml.load`` with ``Loader=yaml.SafeLoader`` or ``Loader=SafeLoader`` is safe
28
+ and will NOT be flagged.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import ast
34
+
35
+ from codeguard.engine.finding import Category, Finding, Severity
36
+ from codeguard.engine.registry import REGISTRY
37
+ from codeguard.engine.rule import AstRule
38
+ from codeguard.rules._pyimports import ImportMap
39
+
40
+ # (module, method) pairs that are always unsafe
41
+ _UNSAFE_CALLS: frozenset[tuple[str, str]] = frozenset(
42
+ {
43
+ ("pickle", "loads"),
44
+ ("pickle", "load"),
45
+ ("_pickle", "loads"),
46
+ ("_pickle", "load"),
47
+ ("marshal", "loads"),
48
+ ("marshal", "load"),
49
+ }
50
+ )
51
+
52
+ _SAFE_LOADERS = frozenset({"SafeLoader", "CSafeLoader"})
53
+
54
+ _FIX_PICKLE = (
55
+ "Never unpickle data from untrusted sources. "
56
+ "Consider JSON, msgpack, or protobuf for cross-process serialization."
57
+ )
58
+
59
+ _FIX_YAML = (
60
+ "Pass Loader=yaml.SafeLoader: yaml.load(data, Loader=yaml.SafeLoader), "
61
+ "or use yaml.safe_load(data)."
62
+ )
63
+
64
+
65
+ class UnsafeDeserializationRule(AstRule):
66
+ """Detect pickle.loads, marshal.loads, and yaml.load without SafeLoader."""
67
+
68
+ id = "CG-SEC-004"
69
+ title = "Unsafe deserialization"
70
+ description = (
71
+ "Deserializing data with pickle, marshal, or yaml.load (without SafeLoader) "
72
+ "can execute arbitrary code if the data source is attacker-controlled."
73
+ )
74
+ severity = Severity.HIGH
75
+ category = Category.SECURITY
76
+ cwe = "CWE-502"
77
+ owasp = "A08:2021 - Software and Data Integrity Failures"
78
+
79
+ def check_ast(self, tree: ast.AST, source: str, filename: str) -> list[Finding]:
80
+ """Find unsafe deserialization calls."""
81
+ findings: list[Finding] = []
82
+ imports = ImportMap.from_tree(tree)
83
+
84
+ for node in ast.walk(tree):
85
+ if not isinstance(node, ast.Call):
86
+ continue
87
+
88
+ module, method = imports.resolve_call(node.func)
89
+ if not module or not method:
90
+ continue
91
+
92
+ # pickle.loads / marshal.loads — always unsafe
93
+ if (module, method) in _UNSAFE_CALLS:
94
+ findings.append(
95
+ self._make_finding(
96
+ node=node,
97
+ filename=filename,
98
+ description=(
99
+ f"{module}.{method}() deserializes arbitrary Python objects. "
100
+ "This is a critical vulnerability if the data is attacker-controlled."
101
+ ),
102
+ fix_suggestion=_FIX_PICKLE,
103
+ )
104
+ )
105
+
106
+ # yaml.load — only unsafe without SafeLoader
107
+ elif module == "yaml" and method == "load":
108
+ if not self._has_safe_loader(node):
109
+ findings.append(
110
+ self._make_finding(
111
+ node=node,
112
+ filename=filename,
113
+ description=(
114
+ "yaml.load() without Loader=SafeLoader can instantiate "
115
+ "arbitrary Python objects from the YAML input."
116
+ ),
117
+ fix_suggestion=_FIX_YAML,
118
+ )
119
+ )
120
+
121
+ return findings
122
+
123
+ # ------------------------------------------------------------------
124
+ # Helpers
125
+ # ------------------------------------------------------------------
126
+
127
+ @staticmethod
128
+ def _has_safe_loader(node: ast.Call) -> bool:
129
+ """Return True if Loader=SafeLoader (or equivalent) is present."""
130
+ # Keyword argument: yaml.load(data, Loader=yaml.SafeLoader)
131
+ for kw in node.keywords:
132
+ if kw.arg == "Loader":
133
+ val = kw.value
134
+ loader_name = (
135
+ val.attr
136
+ if isinstance(val, ast.Attribute)
137
+ else (val.id if isinstance(val, ast.Name) else "")
138
+ )
139
+ if loader_name in _SAFE_LOADERS:
140
+ return True
141
+
142
+ # Positional argument: yaml.load(data, yaml.SafeLoader)
143
+ if len(node.args) >= 2:
144
+ arg = node.args[1]
145
+ loader_name = (
146
+ arg.attr
147
+ if isinstance(arg, ast.Attribute)
148
+ else (arg.id if isinstance(arg, ast.Name) else "")
149
+ )
150
+ if loader_name in _SAFE_LOADERS:
151
+ return True
152
+
153
+ return False
154
+
155
+
156
+ REGISTRY.register(UnsafeDeserializationRule())
@@ -0,0 +1,157 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """CG-SEC-005 — a non-literal command run through a shell.
3
+
4
+ Two cases, both CWE-78 / OWASP A03:2021:
5
+
6
+ - ``subprocess.run`` / ``call`` / ``Popen`` / ``check_output`` / ``check_call``
7
+ with ``shell=True`` and a non-literal command.
8
+ - ``os.system`` / ``os.popen`` / ``subprocess.getoutput`` /
9
+ ``subprocess.getstatusoutput`` with a non-literal command — these always use
10
+ a shell, there is no ``shell=`` keyword to check.
11
+
12
+ Import aliases are resolved, so ``from os import system`` and
13
+ ``import subprocess as sp`` are covered.
14
+
15
+ Why this matters
16
+ ----------------
17
+ When a command string is interpreted by the OS shell and any part of it is
18
+ attacker-controlled, the attacker can inject arbitrary shell commands.
19
+
20
+ AI models routinely generate ``subprocess.run(f"git {user_arg}", shell=True)``
21
+ because it looks clean and concise. It is not.
22
+
23
+ Safe patterns NOT flagged
24
+ -------------------------
25
+ - ``subprocess.run("ls -la", shell=True)`` — literal string, no injection vector
26
+ - ``subprocess.run(["git", "status"])`` — list form without shell=True
27
+ - ``subprocess.run(cmd, shell=False)`` — shell disabled
28
+
29
+ False-positive guidance
30
+ -----------------------
31
+ If the command is constructed from entirely trusted, validated values you can
32
+ suppress: ``# codeguard: ignore[CG-SEC-005]``
33
+
34
+ Better: switch to the list form (``shell=False``).
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import ast
40
+
41
+ from codeguard.engine.finding import Category, Finding, Severity
42
+ from codeguard.engine.registry import REGISTRY
43
+ from codeguard.engine.rule import AstRule
44
+ from codeguard.rules._pyimports import ImportMap
45
+
46
+ # subprocess entry points where a shell is only used when shell=True is passed.
47
+ _SHELL_OPTIONAL: frozenset[tuple[str, str]] = frozenset(
48
+ {
49
+ ("subprocess", "run"),
50
+ ("subprocess", "call"),
51
+ ("subprocess", "Popen"),
52
+ ("subprocess", "check_output"),
53
+ ("subprocess", "check_call"),
54
+ }
55
+ )
56
+
57
+ # Calls that ALWAYS run their argument through a shell -- no shell= keyword.
58
+ _ALWAYS_SHELL: frozenset[tuple[str, str]] = frozenset(
59
+ {
60
+ ("os", "system"),
61
+ ("os", "popen"),
62
+ ("subprocess", "getoutput"),
63
+ ("subprocess", "getstatusoutput"),
64
+ }
65
+ )
66
+
67
+ _FIX = (
68
+ "Pass the command as a list with shell=False: "
69
+ "subprocess.run(['git', 'status'], shell=False). "
70
+ "If a shell is required, validate and sanitize every interpolated value with shlex.quote()."
71
+ )
72
+
73
+
74
+ class ShellInjectionRule(AstRule):
75
+ """Detect subprocess calls with shell=True and a non-literal command."""
76
+
77
+ id = "CG-SEC-005"
78
+ title = "subprocess with shell=True and dynamic command"
79
+ description = (
80
+ "subprocess is called with shell=True and the command argument is not a "
81
+ "string literal. If any part of the command is attacker-controlled, this "
82
+ "enables shell command injection."
83
+ )
84
+ severity = Severity.HIGH
85
+ category = Category.SECURITY
86
+ cwe = "CWE-78"
87
+ owasp = "A03:2021 - Injection"
88
+
89
+ def check_ast(self, tree: ast.AST, source: str, filename: str) -> list[Finding]:
90
+ """Find shell command injection via subprocess / os.system."""
91
+ findings: list[Finding] = []
92
+ imports = ImportMap.from_tree(tree)
93
+
94
+ for node in ast.walk(tree):
95
+ if not isinstance(node, ast.Call):
96
+ continue
97
+
98
+ target = imports.resolve_call(node.func)
99
+ always_shell = target in _ALWAYS_SHELL
100
+ shell_optional = target in _SHELL_OPTIONAL
101
+
102
+ if not always_shell and not shell_optional:
103
+ continue
104
+ if shell_optional and not self._has_shell_true(node):
105
+ continue
106
+ if self._command_is_literal(node):
107
+ continue
108
+
109
+ module, method = target
110
+ findings.append(
111
+ self._make_finding(
112
+ node=node,
113
+ filename=filename,
114
+ description=(
115
+ f"{module}.{method}() runs a non-literal command through a shell. "
116
+ "If any part of the command is attacker-controlled, this enables "
117
+ "shell command injection."
118
+ ),
119
+ fix_suggestion=_FIX,
120
+ )
121
+ )
122
+
123
+ return findings
124
+
125
+ # ------------------------------------------------------------------
126
+ # Helpers
127
+ # ------------------------------------------------------------------
128
+
129
+ @staticmethod
130
+ def _has_shell_true(node: ast.Call) -> bool:
131
+ """Return True if shell=True is explicitly passed."""
132
+ for kw in node.keywords:
133
+ if kw.arg == "shell":
134
+ val = kw.value
135
+ # shell=True or shell=1
136
+ if isinstance(val, ast.Constant) and val.value:
137
+ return True
138
+ return False
139
+
140
+ @staticmethod
141
+ def _command_is_literal(node: ast.Call) -> bool:
142
+ """Return True if the command arg is a plain string literal (safe case)."""
143
+ if not node.args:
144
+ # All keyword args — look for args=
145
+ for kw in node.keywords:
146
+ if kw.arg == "args":
147
+ return isinstance(kw.value, ast.Constant)
148
+ return True # no command at all — not our concern
149
+
150
+ first = node.args[0]
151
+ # List of literals is fine too: ["ls", "-la"]
152
+ if isinstance(first, ast.List):
153
+ return all(isinstance(elt, ast.Constant) for elt in first.elts)
154
+ return isinstance(first, ast.Constant)
155
+
156
+
157
+ REGISTRY.register(ShellInjectionRule())
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.5
2
+ Name: codeguard-cli
3
+ Version: 2.0.0
4
+ Summary: Fast, offline multi-language static analysis for security anti-patterns (Python, JS, TS)
5
+ Project-URL: Homepage, https://github.com/mevichitra/codeguard
6
+ Project-URL: Bug Tracker, https://github.com/mevichitra/codeguard/issues
7
+ Project-URL: Security, https://github.com/mevichitra/codeguard/security
8
+ Project-URL: Documentation, https://github.com/mevichitra/codeguard/tree/main/docs
9
+ Author: CodeGuard Contributors
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: ast,linting,sarif,sast,security,static-analysis
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: click>=8.1
26
+ Requires-Dist: pathspec>=0.12
27
+ Requires-Dist: platformdirs>=4.0
28
+ Requires-Dist: rich>=13.0
29
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
30
+ Requires-Dist: tree-sitter-javascript>=0.23
31
+ Requires-Dist: tree-sitter-typescript>=0.23
32
+ Requires-Dist: tree-sitter>=0.23
33
+ Provides-Extra: dev
34
+ Requires-Dist: build>=1.2; extra == 'dev'
35
+ Requires-Dist: mypy>=1.7; extra == 'dev'
36
+ Requires-Dist: pip-audit>=2.7; extra == 'dev'
37
+ Requires-Dist: pre-commit>=3.6; extra == 'dev'
38
+ Requires-Dist: pytest-cov>=4.1; extra == 'dev'
39
+ Requires-Dist: pytest>=7.4; extra == 'dev'
40
+ Requires-Dist: ruff>=0.4; extra == 'dev'
41
+ Requires-Dist: towncrier>=24.8; extra == 'dev'
42
+ Provides-Extra: docs
43
+ Requires-Dist: mkdocs-gen-files>=0.5; extra == 'docs'
44
+ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # CodeGuard
48
+
49
+ Fast, offline static analysis that finds security anti-patterns in **Python, JavaScript, and TypeScript** — and drops into every gate of your workflow (editor, pre-commit, PR/CI, scheduled audit) from a single config file.
50
+
51
+ **Status: 2.0 (beta).** Rule IDs, the `Finding`/JSON schema, config keys, and exit codes are a stable contract from 2.0. See [migration notes](docs/migration-v2.md) if you used the 0.1 alpha.
52
+
53
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
54
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
55
+ [![CI](https://github.com/mevichitra/codeguard/actions/workflows/ci.yml/badge.svg)](https://github.com/mevichitra/codeguard/actions)
56
+ [![PyPI](https://img.shields.io/pypi/v/codeguard-cli.svg)](https://pypi.org/project/codeguard-cli/)
57
+
58
+ ---
59
+
60
+ ## What it does
61
+
62
+ CodeGuard scans **Python, JavaScript, and TypeScript** and reports security findings. Each finding has a stable rule ID, severity, CWE/OWASP mapping, and a plain-English fix suggestion.
63
+
64
+ Current rules (see [docs/rules/](docs/rules/) for detail):
65
+
66
+ | ID | What it catches | Severity | CWE | Languages |
67
+ |---|---|---|---|---|
68
+ | CG-SEC-001 | SQL built with f-strings / `%` / `.format()` | HIGH | CWE-89 | Python |
69
+ | CG-SEC-002 | Hardcoded passwords, API keys, tokens | HIGH | CWE-798 | Python |
70
+ | CG-SEC-003 | `eval()` / `exec()` on non-literal input | HIGH | CWE-95 | Python |
71
+ | CG-SEC-004 | `pickle.loads` / `yaml.load` without SafeLoader | HIGH | CWE-502 | Python |
72
+ | CG-SEC-005 | `subprocess(..., shell=True)` with non-literal args | HIGH | CWE-78 | Python |
73
+ | CG-SEC-101 | `eval` / `new Function` / string timers on dynamic input | HIGH | CWE-95 | JS, TS |
74
+ | CG-SEC-102 | `child_process.exec` with a dynamic command | HIGH | CWE-78 | JS, TS |
75
+ | CG-SEC-103 | `innerHTML` / `document.write` assigned a non-literal | HIGH | CWE-79 | JS, TS |
76
+ | CG-SEC-104 | `dangerouslySetInnerHTML` with a non-literal value | HIGH | CWE-79 | JS, TS |
77
+ | CG-SEC-105 | Hardcoded passwords, API keys, tokens | HIGH | CWE-798 | JS, TS |
78
+ | CG-SEC-106 | `Math.random()` used for a token / secret / id | MEDIUM | CWE-338 | JS, TS |
79
+
80
+ ### What it does not do (yet)
81
+
82
+ - Baseline / diff scanning, a CI-native command, packaged distribution (planned for v2.0)
83
+ - AI-generated-code detection (deferred to a post-2.0 experimental module)
84
+ - Web dashboard or REST API
85
+
86
+ ---
87
+
88
+ ## Install
89
+
90
+ ```bash
91
+ pipx install codeguard-cli # or: uv tool install codeguard-cli
92
+ pip install codeguard-cli # into the current environment
93
+
94
+ # or from source:
95
+ git clone https://github.com/mevichitra/codeguard
96
+ cd codeguard
97
+ pip install -e ".[dev]"
98
+ ```
99
+
100
+ The PyPI project is `codeguard-cli`; the installed command is `codeguard`.
101
+
102
+ Requires Python 3.10+. No database, no Redis, no Docker needed.
103
+
104
+ ---
105
+
106
+ ## Usage
107
+
108
+ ```bash
109
+ # Scan a file or directory
110
+ codeguard scan myproject/
111
+
112
+ # Output as JSON
113
+ codeguard scan myproject/ --format json
114
+
115
+ # Output as SARIF (for GitHub code scanning)
116
+ codeguard scan myproject/ --format sarif > results.sarif
117
+
118
+ # Only run specific rules
119
+ codeguard scan myproject/ --rule CG-SEC-001 --rule CG-SEC-002
120
+
121
+ # Only report HIGH and above
122
+ codeguard scan myproject/ --severity high
123
+ ```
124
+
125
+ Example output (human format):
126
+
127
+ ```
128
+ myproject/auth.py:12:4 [CG-SEC-001] HIGH SQL query built with string formatting
129
+ → Use parameterized queries: cursor.execute("SELECT ... WHERE id = %s", (user_id,))
130
+
131
+ myproject/config.py:5:0 [CG-SEC-002] HIGH Hardcoded secret: password
132
+ → Load secrets from environment variables or a secrets manager.
133
+
134
+ 2 findings (2 high, 0 medium, 0 low)
135
+ ```
136
+
137
+ Exit codes: `0` = no findings, `1` = findings found, `2` = error.
138
+
139
+ ### Inline suppression
140
+
141
+ ```python
142
+ query = f"SELECT * FROM users WHERE id = {uid}" # codeguard: ignore[CG-SEC-001]
143
+ ```
144
+
145
+ Suppressed findings still appear with `suppressed: true` in JSON/SARIF output.
146
+
147
+ ### Config file
148
+
149
+ Place a `codeguard.toml` in your project root:
150
+
151
+ ```toml
152
+ [codeguard]
153
+ exclude = ["tests/", "migrations/"]
154
+ severity = "medium" # ignore findings below this level
155
+
156
+ [codeguard.rules]
157
+ disabled = ["CG-SEC-002"] # not yet
158
+ ```
159
+
160
+ _(Config file support is on the roadmap; not yet implemented.)_
161
+
162
+ ---
163
+
164
+ ## CI integration
165
+
166
+ ### GitHub Actions
167
+
168
+ ```yaml
169
+ - name: Run CodeGuard
170
+ run: |
171
+ pip install codeguard-cli
172
+ codeguard scan src/ --format sarif > codeguard.sarif
173
+
174
+ - name: Upload SARIF
175
+ uses: github/codeql-action/upload-sarif@v3
176
+ with:
177
+ sarif_file: codeguard.sarif
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Contributing
183
+
184
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for how to add a rule or file a bug.
185
+ Adding a rule requires: one rule file + two test fixtures (vulnerable + safe). See the [rule authoring guide](CONTRIBUTING.md#adding-a-rule).
186
+
187
+ ---
188
+
189
+ ## Project status and roadmap
190
+
191
+ **2.0 (beta).** Rule IDs, the `Finding` / JSON schema, config keys, and the exit-code
192
+ contract are stable. Full details in [the changelog](CHANGELOG.md) and
193
+ [docs/](https://mevichitra.github.io/codeguard/).
194
+
195
+ On the roadmap:
196
+
197
+ 1. Light intraprocedural taint tracking (source → sink, sanitizer-aware) to cut false positives further
198
+ 2. A Semgrep-compatible YAML rule subset for custom rules
199
+ 3. Autofix (`--fix`) for the safe-fix rules
200
+ 4. Standalone single-file binaries and a Homebrew tap
201
+ 5. An LSP server for editor integration
202
+ 6. Optional, offline AI-assisted triage (confidence + rationale, no code leaves the machine)
203
+
204
+ ---
205
+
206
+ ## License
207
+
208
+ Apache-2.0. See [LICENSE](LICENSE).
209
+
210
+ Contributions are accepted under the [Developer Certificate of Origin](https://developercertificate.org/) (DCO). Sign your commits with `git commit -s`.
@@ -0,0 +1,52 @@
1
+ codeguard/__init__.py,sha256=rSpNWh22tyLJb077nuoPh0_u-88k3arwSFUtAktUpzU,205
2
+ codeguard/py.typed,sha256=hnBZ2f5aEYic_FIpxxOxTCC1g1rZ2U4caFMSikgm170,57
3
+ codeguard/cli/__init__.py,sha256=RWO3W_psF33Drjj6RmfHeDVJE_Ws6lN6n9sN5tnoOA4,67
4
+ codeguard/cli/_run.py,sha256=Vj63yfWXhhhchXqtZjEU5wwmGMipbQy-wS0L1oWrcTE,8313
5
+ codeguard/cli/commands.py,sha256=OyUocW-INriCV_fvubxOHGhTtTMKlA_VtdY38Gdxfkw,13679
6
+ codeguard/cli/formatters.py,sha256=CKsN8tV_LDRIeyKzCkWZUuunyA3lNyxQZDpLnI0JVRs,14869
7
+ codeguard/cli/main.py,sha256=jTUR1ZCI7sl6IjwpTPhy2KMvNyFXgB7tiTh_LAWiPV4,6004
8
+ codeguard/config/__init__.py,sha256=Evh6UYwdh4FBhp6HYMEHp-XwwM9ei8zttZeWM8i04z4,393
9
+ codeguard/config/loader.py,sha256=3R4ElUqOX9TuN-bAZa-70QwjlNqqnLkWuqEKorOgzCA,2627
10
+ codeguard/config/schema.py,sha256=m5CliVIMFy6TCKFHaj0SjmgD3fufEVaNwttzQCxGQEY,6813
11
+ codeguard/engine/__init__.py,sha256=W3eQbTLlHaQPEiCkpdvzIjh4Xlvz75wbz28kxwvnPow,556
12
+ codeguard/engine/baseline.py,sha256=zzEVkLAMQO2ezftWkt361D_-Aqiz9HOFU4Rx26pi1io,4577
13
+ codeguard/engine/context.py,sha256=Eaum1v8lGjCc0tFoNuJPoYnjsvuLOaU0p5M3uM7LTn8,1766
14
+ codeguard/engine/discovery.py,sha256=mUONVr4TmcK_5MkfkPtQhdZ1x7nivU3KnDODrWzequI,4806
15
+ codeguard/engine/finding.py,sha256=s-9DH5jAS75iaqXj99D5hYykyHXD8N5fSehrGMwkCkc,6301
16
+ codeguard/engine/fingerprint.py,sha256=kGOq2hvov-gZSvLkm5KPDIF5V2vAUnCai14jKBrJeMs,3178
17
+ codeguard/engine/gitdiff.py,sha256=NJOXPLK7LZ30gWcxBGoQXIQbJ1Uk1MKcVYRzv2IBUKE,2418
18
+ codeguard/engine/policy.py,sha256=Zv9iNGNFA3xnOEm_gZB59emLchD4dIObVxrxsz8Frz4,2574
19
+ codeguard/engine/registry.py,sha256=161GqYUkZZvTb8q5CG4HjT-LDkdLyS3aIDxFXQYJDQU,2439
20
+ codeguard/engine/rule.py,sha256=Mk_K_z2fJSUGLO7GzhuUvKyb9g3zPanEEg0f1hLDqLg,6657
21
+ codeguard/engine/runner.py,sha256=SFM5_ls2UkXzSwtzi4QU5I71sGSMO28Vjzd9B38qmQg,9949
22
+ codeguard/engine/suppressions.py,sha256=MsN8yqLbOMzxvkYoSzlj0Zywo_cLRnP8knk-ewCbUx4,4053
23
+ codeguard/lang/__init__.py,sha256=OE27PN1n9kIZsCSc9u6R4LT6zvXjznuCGIQPXzi9ZZQ,1160
24
+ codeguard/lang/base.py,sha256=WKVolBMOrPh-lbuABuHFnFsnkgHtmSJ7A4NKk-v-d4E,2266
25
+ codeguard/lang/javascript.py,sha256=-WavoLvHOvsHv0zSAGKne_HUEMfTyhz1oNc_Gg9Gv14,536
26
+ codeguard/lang/node.py,sha256=Nv7sw3TIovgQyJBdRQXtnU_de9sgdYE48ngbqCmhMw0,5341
27
+ codeguard/lang/python_ast.py,sha256=KGjOntGqygKVPuspHEWrJw2zrniOLeq7VZmAtd03JQM,933
28
+ codeguard/lang/registry.py,sha256=w949r0nZbBmymDPAEm7_Bh6iBy4jm39pgPbkAqcHsPw,1156
29
+ codeguard/lang/treesitter.py,sha256=rZh4Fxs5rNj_UEiwlzTaaU2hRAMX_du3uBRsKFpVVOE,3151
30
+ codeguard/lang/typescript.py,sha256=LIJSnmXOWgljOQNbs07IJ6DsKoaexivwW-WJnZimpJY,686
31
+ codeguard/rules/__init__.py,sha256=iuF7srUvwALv71dWwcWtzwcQ01rLlRbKctZKOADiniI,264
32
+ codeguard/rules/_jsnodes.py,sha256=mSPAMrgwBANF6ayuHsQGi0FB_A7v2eTZlXr9Bofg1pE,2439
33
+ codeguard/rules/_pyimports.py,sha256=tiPgyb3sDmmja87MNWUb-L8FqchFoy6qcjjn8m0yLig,2475
34
+ codeguard/rules/javascript/__init__.py,sha256=_pyPrFeDzfgih0BhL4xMYKGUCSnOqC0g2-kPJeIyUcc,479
35
+ codeguard/rules/javascript/cg_sec_101_dynamic_code.py,sha256=HAxCUM1wkRSKzD-dKPXHq9IZD1OUhNQZ47I-L5cUtU0,3505
36
+ codeguard/rules/javascript/cg_sec_102_child_process.py,sha256=ThorafEt6U6SW4s6ArwiSTnO7OHSMl07CG3lnn8Q7sM,2238
37
+ codeguard/rules/javascript/cg_sec_103_dom_xss.py,sha256=AJs-qtPyT_uGlDNs08rDnE-omb2VRAoGsB4pVluYNqE,2728
38
+ codeguard/rules/javascript/cg_sec_104_react_dangerous_html.py,sha256=XA5Vyy9x9Jt_lwOJUJi6rD7roOhiFN1Wcy3iB-93HDY,1985
39
+ codeguard/rules/javascript/cg_sec_105_hardcoded_secret.py,sha256=kR6nOPOx2WXbtEi052qb5TNE5CAJ6TFT--kvyZkOGl8,2750
40
+ codeguard/rules/javascript/cg_sec_106_weak_random.py,sha256=JeSDbFmQvcvuQMCYLk9GsqcYZ78k9ZqotYhz52WYUJU,3028
41
+ codeguard/rules/meta/__init__.py,sha256=KWvs-SmJg5BmvOPZWLZVNIB1x6R86p7_vOmoSRe9w70,1964
42
+ codeguard/rules/security/__init__.py,sha256=2XrbMlU0eXPO3ZgHPt_ccPqkjEhkJllbfirnSkGsSPM,438
43
+ codeguard/rules/security/cg_sec_001_sql_injection.py,sha256=49QCPIiwXNTTa48Oh7knhSlHt6eHUspIdWzTfCJSEKw,4025
44
+ codeguard/rules/security/cg_sec_002_hardcoded_secrets.py,sha256=aLlKaCTDuYDON6CWEy18UNG4qPeQF_R5kD8PHOJZvlA,7423
45
+ codeguard/rules/security/cg_sec_003_eval_exec.py,sha256=EiP_oak51iwURYu6mR5KEDI0csO-pxUohiMu6Q-7m2Y,3625
46
+ codeguard/rules/security/cg_sec_004_unsafe_deserialization.py,sha256=kYSftvwI49pYraOPBtReqcejLAoxUxresY4hihqD-nw,5559
47
+ codeguard/rules/security/cg_sec_005_shell_injection.py,sha256=tUn6m2bmZ1zGpWXDRJOJQm-DALcUmPpefWlLiu2qcAM,5590
48
+ codeguard_cli-2.0.0.dist-info/METADATA,sha256=vD1jmTm0uCvlrShjwGyuwihMdCAkgZ4mO2b5sLqFT9Q,7496
49
+ codeguard_cli-2.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
50
+ codeguard_cli-2.0.0.dist-info/entry_points.txt,sha256=XOpCnyTVSj3IGv_BjHUBz6VvLV1HTWqFYi4M6FOHxw4,53
51
+ codeguard_cli-2.0.0.dist-info/licenses/LICENSE,sha256=8MbEbZBqWFGR5HLQoPl6VQcU5WOT027U_9PD0bTsXmo,10316
52
+ codeguard_cli-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ codeguard = codeguard.cli.main:cli