## https://sploitus.com/exploit?id=4260277F-0218-5B04-B078-5D2272CBD1A8
# codesentry
Static analysis for Python and JavaScript. Finds injection flaws, XSS sinks,
hardcoded credentials and coding-standard violations, and emits results in a
format CI systems can act on.
Standard library only. No install step, no dependencies.
```bash
python3 -m codesentry samples/
```
---
## Usage
```bash
# scan a tree, human-readable output
python3 -m codesentry src/
# HTML report as a build artifact
python3 -m codesentry src/ --format html --output reports/scan.html
# SARIF for GitHub code scanning / IDE annotations
python3 -m codesentry src/ --format sarif --output results.sarif --fail-on high
# security findings only, above medium
python3 -m codesentry src/ --category security --min-severity medium
# one rule, or everything except a family
python3 -m codesentry src/ --rule PY-SEC-001
python3 -m codesentry src/ --exclude-rule PY-STD
# load team rules from outside the package
python3 -m codesentry src/ --rubric-dir ./team-rubrics --plugin-dir ./team-rules
# what is loaded right now
python3 -m codesentry --list-rules
```
### Exit codes
| Code | Meaning |
|------|---------|
| `0` | No finding at or above `--fail-on` (default `high`) |
| `1` | Threshold breached β fail the build |
| `2` | Internal error: a rule crashed or a file could not be prepared |
| `130`| Interrupted |
`2` is deliberately distinct from `1`. "The scan found problems" and "the scan
did not fully run" call for different responses, and collapsing them is how a
silently degraded scanner gets mistaken for a clean bill of health.
### Suppressions
```python
subprocess.run(cmd, shell=True) # codesentry: ignore[PY-SEC-003]
# codesentry: ignore[PY-SEC-009]
digest = hashlib.md5(blob).hexdigest()
value = eval(expr) # codesentry: ignore
```
A bare `ignore` suppresses everything on that line; the bracketed form
suppresses only the named rules, so an unrelated flaw on the same line is still
reported. `# codesentry: ignore-file` near the top skips the file. Suppressed
findings are counted in the summary rather than vanishing, and
`--no-suppressions` audits what is being hidden.
---
## Layout
```
codesentry/
models.py Severity, Finding, RuleMeta, Diagnostic, ScanResult
context.py decoding, comment/string masking, AST parsing, line index
registry.py rule registration, discovery, rubric loading <- extension point
engine.py file walk, rule dispatch, error isolation, dedup, suppression
cli.py argument parsing and wiring
rules/ auto-imported rule modules
reporters/ console, structured (JSON + SARIF), html
rubrics/ declarative JSON rules
samples/ vulnerable fixtures, a clean counterpart, edge cases
tests/ 66 tests
```
The engine has no knowledge of any rule. There is no occurrence of `SQL`,
`XSS`, `eval` or `snake_case` anywhere in `engine.py`, and adding a rule never
requires editing it. Rules are found through the registry and described by
their own metadata, which is also what drives filtering, report grouping and
severity thresholds.
---
## Adding a rule
Three routes, in increasing order of power.
### 1. A rubric entry β no Python
Add an object to any `.json` file in `rubrics/`:
```json
{
"id": "TEAM-001",
"name": "requests.get without a timeout",
"description": "A request with no timeout can hang a worker indefinitely.",
"languages": ["python"],
"severity": "medium",
"category": "reliability",
"pattern": "requests\\.get\\((?![^)]*timeout)",
"message": "requests.get() has no timeout",
"remediation": "Pass timeout=(connect, read), e.g. requests.get(url, timeout=(3, 10))."
}
```
That is the whole change. Rubrics are validated at load time, so a typo is a
named error before the scan starts rather than a mystery during it.
Keys beyond the required five (`id`, `name`, `languages`, `severity`, `pattern`):
| Key | Effect |
|-----|--------|
| `scope` | `code_only` (default), `masked` (strings visible), `raw` (comments visible) |
| `exclude_pattern` | Drop the hit if this also matches the line |
| `require_pattern` | Only apply the rule to files containing this |
| `skip_minified` | Skip bundled assets (default `true`) |
| `flags` | `ignorecase`, `multiline`, `dotall` |
| `max_hits_per_file` | Cap runaway matches (default 25) |
| `message` | Supports `{match}` and `{1}` substitution |
### 2. A decorated function β full AST access
Drop a module into `codesentry/rules/`. It is imported automatically.
```python
import ast
from ..models import make_finding
from ..registry import rule
@rule(
id="PY-SEC-020",
name="Flask route without authentication",
description="A view registered under /admin with no auth decorator.",
languages=["python"],
severity="high",
cwe="CWE-306",
remediation="Add @login_required, or move the route behind an authenticated blueprint.",
)
def unauthenticated_admin_route(ctx, meta):
if ctx.tree is None: # file did not parse; text rules still ran
return
for node in ast.walk(ctx.tree):
if isinstance(node, ast.FunctionDef) and _is_admin_route(node):
yield make_finding(meta, ctx, node.lineno, f"{node.name}() is unauthenticated")
```
Severity, CWE, remediation text and the code snippet are filled in from
metadata by `make_finding`, so the rule body only describes what it found.
### 3. A plugin directory β rules outside the repo
```bash
python3 -m codesentry src/ --plugin-dir ./company-rules
```
Same `@rule` decorator, loaded from loose `.py` files.
### What the context gives a rule
```python
ctx.source # raw text
ctx.masked # comments blanked, strings intact
ctx.code_only # comments and string bodies blanked
ctx.tree # Python AST, or None if the file did not parse
ctx.line_text(n) # 1-indexed, never raises
ctx.position(off) # (line, column) for any offset in any view
ctx.in_string(off), ctx.in_comment(off)
ctx.is_minified
```
Blanking replaces characters with spaces rather than deleting them, so an
offset in `code_only` still maps to the correct line and column in the file.
---
## Tests
```bash
python3 -m unittest discover -s tests -v
```
66 tests. Rule tests assert true negatives as hard as true positives: a
parameterised query, `yaml.safe_load`, `usedforsecurity=False`, `x != null`,
`os.environ['KEY']` and a static `innerHTML` literal must all produce nothing.
A scanner that cries wolf on correct code trains people to ignore it.