a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
439 lines
20 KiB
Go
439 lines
20 KiB
Go
package astquery
|
|
|
|
import "github.com/zzet/gortex/internal/parser"
|
|
|
|
// Additional Bandit-parity Python rules — split off from rules_sast_python.go
|
|
// so each file stays under ~1500 LOC. Plugin ID references in
|
|
// References:[] map back to the Bandit plugin numbers for cross-check.
|
|
|
|
func init() {
|
|
registerPythonBanditExtras()
|
|
registerPythonCryptoKeys()
|
|
registerPythonFrameworkExtras()
|
|
registerPythonMongoRedis()
|
|
registerPythonTryExceptPass()
|
|
registerPythonRequestsTimeout()
|
|
registerPythonHTTPSConnection()
|
|
registerPythonCElementTreeImport()
|
|
registerPythonTempnam()
|
|
registerPythonPyCryptoDeprecated()
|
|
registerPythonSSLNoVersion()
|
|
registerPythonLinuxCmdWildcard()
|
|
registerPythonMarkupSafe()
|
|
registerPythonJsonPickle()
|
|
registerPythonCGIDeprecated()
|
|
registerPythonHardcodedAssignment()
|
|
registerPythonSubprocessNoShellAudit()
|
|
registerPythonNumpyRandomSeed()
|
|
}
|
|
|
|
// 1. try/except: pass — Bandit B110
|
|
func registerPythonTryExceptPass() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-try-except-pass",
|
|
Description: "`except: pass` — silently swallow every exception. Hides production bugs and breaks observability. Log the exception or narrow the except clause.",
|
|
Severity: "info",
|
|
CWE: "CWE-703",
|
|
Tags: []string{"exception", "swallow"},
|
|
References: []string{"bandit:B110"},
|
|
Pat: map[string]string{
|
|
"python": `((except_clause (block (pass_statement) @body)) @match)`,
|
|
},
|
|
})
|
|
}
|
|
|
|
// 2. requests without timeout — Bandit B113
|
|
func registerPythonRequestsTimeout() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-requests-no-timeout",
|
|
Description: "`requests.get|post|...` without a `timeout=` kwarg — defaults to no timeout, lets a slow / unresponsive server wedge the caller indefinitely. Pass `timeout=(connect_s, read_s)`.",
|
|
Severity: "warning",
|
|
CWE: "CWE-400",
|
|
Tags: []string{"availability", "timeout"},
|
|
References: []string{"bandit:B113"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute object: (identifier) @mod attribute: (identifier) @attr)
|
|
arguments: (argument_list) @args) @match
|
|
(#match? @mod "^(requests|httpx)$")
|
|
(#match? @attr "^(get|post|put|delete|patch|head|options|request)$"))`,
|
|
},
|
|
PostFilter: func(qr parser.QueryResult, _ []byte) bool {
|
|
a, ok := qr.Captures["args"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
return !containsStr(a.Text, "timeout")
|
|
},
|
|
})
|
|
}
|
|
|
|
// 3. HTTPSConnection no context — Bandit B309
|
|
func registerPythonHTTPSConnection() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-httpsconnection-no-context",
|
|
Description: "`httplib.HTTPSConnection(...)` / `http.client.HTTPSConnection(...)` without an explicit `context=` ssl context — TLS verification depends on Python version defaults. Pass `context=ssl.create_default_context()`.",
|
|
Severity: "info",
|
|
CWE: "CWE-295",
|
|
Tags: []string{"tls"},
|
|
References: []string{"bandit:B309"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute object: (identifier) @mod attribute: (identifier) @attr)
|
|
arguments: (argument_list) @args) @match
|
|
(#match? @mod "^(httplib|http\\.client)$")
|
|
(#eq? @attr "HTTPSConnection"))`,
|
|
},
|
|
PostFilter: func(qr parser.QueryResult, _ []byte) bool {
|
|
a, ok := qr.Captures["args"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
return !containsStr(a.Text, "context=")
|
|
},
|
|
})
|
|
}
|
|
|
|
// 4. cElementTree import — Bandit B313 (alias path)
|
|
func registerPythonCElementTreeImport() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-cElementTree-import",
|
|
Description: "`from xml.etree.cElementTree import ...` — Python 2 era C-accelerated stdlib XML parser. XXE-vulnerable; use `defusedxml.ElementTree`.",
|
|
Severity: "error",
|
|
CWE: "CWE-611",
|
|
Tags: []string{"xxe", "xml", "deprecated"},
|
|
References: []string{"bandit:B313"},
|
|
Pat: map[string]string{
|
|
"python": `((import_from_statement module_name: (dotted_name) @mod) @match (#eq? @mod "xml.etree.cElementTree"))
|
|
((import_statement name: (dotted_name) @mod) @match (#eq? @mod "xml.etree.cElementTree"))`,
|
|
},
|
|
})
|
|
}
|
|
|
|
// 5. os.tempnam / os.tmpnam — Bandit B325
|
|
func registerPythonTempnam() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-os-tempnam",
|
|
Description: "`os.tempnam()` / `os.tmpnam()` — race-prone temp-file name generation, deprecated. Use `tempfile.mkstemp()`.",
|
|
Severity: "warning",
|
|
CWE: "CWE-377",
|
|
Tags: []string{"tempfile", "race"},
|
|
References: []string{"bandit:B325"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute object: (identifier) @mod attribute: (identifier) @attr)) @match
|
|
(#eq? @mod "os") (#match? @attr "^(tempnam|tmpnam)$"))`,
|
|
},
|
|
})
|
|
}
|
|
|
|
// 6. pyCrypto (the old C library, not pycryptodome) — Bandit B413
|
|
func registerPythonPyCryptoDeprecated() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-pycrypto-deprecated-import",
|
|
Description: "`import Crypto` (the original pyCrypto library, not pycryptodome) — unmaintained since 2013, multiple unpatched buffer-overflow CVEs (CVE-2013-7459 et al.). Migrate to `pycryptodome` (same API) or `cryptography`.",
|
|
Severity: "warning",
|
|
CWE: "CWE-1104",
|
|
Tags: []string{"crypto", "deprecated", "unmaintained"},
|
|
References: []string{"bandit:B413"},
|
|
Pat: map[string]string{
|
|
"python": `((import_statement name: (dotted_name) @mod) @match (#match? @mod "^Crypto(\\.|$)"))`,
|
|
},
|
|
})
|
|
}
|
|
|
|
// 7. ssl.wrap_socket without ssl_version arg — Bandit B504
|
|
func registerPythonSSLNoVersion() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-ssl-wrap-socket-no-version",
|
|
Description: "`ssl.wrap_socket(...)` without an explicit `ssl_version=ssl.PROTOCOL_TLS_CLIENT` — falls back to `PROTOCOL_TLS` which negotiates the highest supported, but older Python versions defaulted to v23 (SSLv2/v3 acceptable). Be explicit. `ssl.wrap_socket` itself is deprecated since 3.7 — use `SSLContext.wrap_socket`.",
|
|
Severity: "warning",
|
|
CWE: "CWE-326",
|
|
Tags: []string{"tls", "weak-protocol", "deprecated"},
|
|
References: []string{"bandit:B504"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute object: (identifier) @mod attribute: (identifier) @attr)) @match
|
|
(#eq? @mod "ssl") (#eq? @attr "wrap_socket"))`,
|
|
},
|
|
})
|
|
}
|
|
|
|
// 8. shell wildcard expansion — Bandit B609
|
|
func registerPythonLinuxCmdWildcard() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-shell-cmd-with-wildcard",
|
|
Description: "Shell command string containing a glob (`*`, `?`) — `chown user *.txt` is shell-expanded; a file named `--reference=/etc/passwd` becomes an argument. Pass an explicit file list to a non-shell invocation.",
|
|
Severity: "warning",
|
|
CWE: "CWE-78",
|
|
Tags: []string{"command-injection", "wildcard"},
|
|
References: []string{"bandit:B609"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute object: (identifier) @mod attribute: (identifier) @attr)
|
|
arguments: (argument_list (string) @cmd)) @match
|
|
(#match? @mod "^(subprocess|os)$") (#match? @cmd "[*?]"))`,
|
|
},
|
|
})
|
|
}
|
|
|
|
// 9. markupsafe.Markup(...) on user input — Bandit B704
|
|
func registerPythonMarkupSafe() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-markupsafe-markup",
|
|
Description: "`markupsafe.Markup(...)` — flags arbitrary text as HTML-safe. With user input it bypasses Jinja2 / Flask escaping and is XSS.",
|
|
Severity: "warning",
|
|
CWE: "CWE-79",
|
|
Tags: []string{"xss", "jinja2", "flask"},
|
|
References: []string{"bandit:B704"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: [(identifier) @fn (attribute attribute: (identifier) @fn)]) @match (#eq? @fn "Markup"))`,
|
|
},
|
|
})
|
|
}
|
|
|
|
// 10. jsonpickle.decode — RCE primitive
|
|
func registerPythonJsonPickle() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-jsonpickle-decode",
|
|
Description: "`jsonpickle.decode(...)` / `jsonpickle.loads(...)` — under the hood unpickles arbitrary class refs. RCE on attacker-controlled input.",
|
|
Severity: "error",
|
|
CWE: "CWE-502",
|
|
Tags: []string{"deserialization"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute object: (identifier) @mod attribute: (identifier) @attr)) @match
|
|
(#eq? @mod "jsonpickle") (#match? @attr "^(decode|loads)$"))`,
|
|
},
|
|
})
|
|
}
|
|
|
|
// 11. cgi module — deprecated, removed in 3.13
|
|
func registerPythonCGIDeprecated() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-cgi-import-deprecated",
|
|
Description: "`import cgi` — deprecated in 3.11, removed in 3.13. Functions are XSS-prone unless every caller escapes; replace with `urllib.parse.parse_qs` + a templating engine that auto-escapes.",
|
|
Severity: "info",
|
|
CWE: "CWE-1104",
|
|
Tags: []string{"deprecated"},
|
|
Pat: map[string]string{
|
|
"python": `((import_statement name: (dotted_name) @mod) @match (#eq? @mod "cgi"))`,
|
|
},
|
|
})
|
|
}
|
|
|
|
// 12. Bare-assignment hardcoded secret — Bandit B105
|
|
func registerPythonHardcodedAssignment() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-hardcoded-credential-assignment",
|
|
Description: "`password = \"...\"` / `secret = \"...\"` / `api_key = \"...\"` at module scope — leaks the credential into source control. Read from env or a secrets manager.",
|
|
Severity: "error",
|
|
CWE: "CWE-798",
|
|
OWASP: "A07:2021-Identification and Authentication Failures",
|
|
Tags: []string{"secrets", "credentials"},
|
|
References: []string{"bandit:B105"},
|
|
Pat: map[string]string{
|
|
"python": `((assignment left: (identifier) @name right: (string) @val) @match
|
|
(#match? @name "(?i)^(password|passwd|secret|api_?key|token|aws_?secret(_?key)?|access_?key|private_?key)$"))`,
|
|
},
|
|
PostFilter: secretLiteralLooksReal,
|
|
})
|
|
}
|
|
|
|
// 13. subprocess.* without shell=True — Bandit B603 (audit hook)
|
|
func registerPythonSubprocessNoShellAudit() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-subprocess-no-shell-audit",
|
|
Description: "`subprocess.Popen|call|run|check_output|check_call(...)` without `shell=True` — the safe form, but still worth flagging when args are user-derived. Audit hook: confirm every argv element is sanitised or from a fixed allow-list.",
|
|
Severity: "info",
|
|
CWE: "CWE-78",
|
|
Tags: []string{"audit-hook", "subprocess"},
|
|
References: []string{"bandit:B603"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute object: (identifier) @mod attribute: (identifier) @attr)) @match
|
|
(#eq? @mod "subprocess") (#match? @attr "^(Popen|call|run|check_output|check_call)$"))`,
|
|
},
|
|
})
|
|
}
|
|
|
|
// 14. numpy.random.seed — predictable
|
|
func registerPythonNumpyRandomSeed() {
|
|
mustRegisterSAST(sastRule{
|
|
Name: "py-numpy-random-fixed-seed",
|
|
Description: "`numpy.random.seed(<literal>)` — fixed-seed numpy random is deterministic across runs. Fine for reproducible experiments, never use a fixed-seed numpy RNG for tokens / IVs / keys.",
|
|
Severity: "info",
|
|
CWE: "CWE-330",
|
|
Tags: []string{"random", "numpy"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute object: (attribute object: (identifier) @np attribute: (identifier) @sub) attribute: (identifier) @attr)
|
|
arguments: (argument_list (integer))) @match
|
|
(#eq? @np "numpy") (#eq? @sub "random") (#eq? @attr "seed"))
|
|
((call function: (attribute object: (attribute object: (identifier) @np attribute: (identifier) @sub) attribute: (identifier) @attr)
|
|
arguments: (argument_list (integer))) @match
|
|
(#eq? @np "np") (#eq? @sub "random") (#eq? @attr "seed"))`,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 15. Bandit-extras catch-all — bind to (host="0.0.0.0"), httpx verify=False,
|
|
// starlette debug, scapy.send tx flooding hooks, asyncpg etc.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func registerPythonBanditExtras() {
|
|
mustRegisterSAST(
|
|
sastRule{
|
|
Name: "py-bind-host-zero",
|
|
Description: "`...(host=\"0.0.0.0\", ...)` — Flask / FastAPI / aiohttp / Tornado application bound to every interface. Almost certainly unintentional in production; bind to `127.0.0.1` unless explicitly public.",
|
|
Severity: "warning",
|
|
CWE: "CWE-605",
|
|
Tags: []string{"bind", "all-interfaces"},
|
|
Pat: map[string]string{
|
|
"python": `((call arguments: (argument_list (keyword_argument name: (identifier) @kw value: (string) @val))) @match
|
|
(#eq? @kw "host") (#match? @val "[\"']?(0\\.0\\.0\\.0|::)[\"']?"))`,
|
|
},
|
|
},
|
|
sastRule{
|
|
Name: "py-pyramid-config-debug",
|
|
Description: "Pyramid `config.add_settings({'pyramid.debug_all': True})` — enables the introspection debug view that leaks app internals.",
|
|
Severity: "warning",
|
|
CWE: "CWE-489",
|
|
Tags: []string{"debug", "pyramid"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute attribute: (identifier) @fn)
|
|
arguments: (argument_list (string) @val)) @match
|
|
(#eq? @fn "add_settings") (#match? @val "pyramid\\.debug"))`,
|
|
},
|
|
},
|
|
sastRule{
|
|
Name: "py-fastapi-debug-true",
|
|
Description: "`fastapi.FastAPI(debug=True)` — exposes stack traces / route table in error responses. Disable in production.",
|
|
Severity: "warning",
|
|
CWE: "CWE-489",
|
|
Tags: []string{"debug", "fastapi"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute object: (identifier) @mod attribute: (identifier) @attr)
|
|
arguments: (argument_list (keyword_argument name: (identifier) @kw value: (true)))) @match
|
|
(#eq? @mod "fastapi") (#eq? @attr "FastAPI") (#eq? @kw "debug"))`,
|
|
},
|
|
},
|
|
sastRule{
|
|
Name: "py-tornado-xss-render",
|
|
Description: "Tornado `RequestHandler.render(...)` with `{% raw user_input %}` template tag — `{% raw %}` skips HTML escaping; with user data this is XSS. Use `{{ var }}` for escaped output.",
|
|
Severity: "info",
|
|
CWE: "CWE-79",
|
|
Tags: []string{"xss", "tornado"},
|
|
Pat: map[string]string{
|
|
"python": `((string) @match (#match? @match "\\{% raw "))`,
|
|
},
|
|
},
|
|
sastRule{
|
|
Name: "py-werkzeug-debug-pin",
|
|
Description: "`werkzeug.debug.DebuggedApplication(app, evalex=True)` — Werkzeug debugger PIN can be bypassed if `WERKZEUG_DEBUG_PIN=off`. Never deploy with `evalex=True`.",
|
|
Severity: "error",
|
|
CWE: "CWE-489",
|
|
Tags: []string{"debug", "werkzeug"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute object: (identifier) @mod attribute: (identifier) @attr)
|
|
arguments: (argument_list . (_) (keyword_argument name: (identifier) @kw value: (true)))) @match
|
|
(#eq? @attr "DebuggedApplication") (#eq? @kw "evalex"))`,
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
// 16. Weak crypto key sizes — Bandit B505
|
|
func registerPythonCryptoKeys() {
|
|
mustRegisterSAST(
|
|
sastRule{
|
|
Name: "py-rsa-weak-key-size",
|
|
Description: "`RSA.generate(<1024)` / `rsa.generate_private_key(public_exponent=..., key_size=<2048)` — 1024-bit RSA factorable by nation-state adversaries; NIST minimum is 2048. Use 3072+ for new deployments.",
|
|
Severity: "warning",
|
|
CWE: "CWE-326",
|
|
Tags: []string{"crypto", "weak-key"},
|
|
References: []string{"bandit:B505"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute object: (identifier) @mod attribute: (identifier) @attr)
|
|
arguments: (argument_list (integer) @bits)) @match
|
|
(#match? @mod "^(RSA|rsa)$") (#eq? @attr "generate") (#match? @bits "^(512|768|1024)$"))
|
|
((call arguments: (argument_list (keyword_argument name: (identifier) @kw value: (integer) @bits))) @match
|
|
(#eq? @kw "key_size") (#match? @bits "^(512|768|1024)$"))`,
|
|
},
|
|
},
|
|
sastRule{
|
|
Name: "py-dsa-weak-key-size",
|
|
Description: "`DSA.generate(<2048)` — 1024-bit DSA is below NIST minimum since 2013.",
|
|
Severity: "warning",
|
|
CWE: "CWE-326",
|
|
Tags: []string{"crypto", "weak-key"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute object: (identifier) @mod attribute: (identifier) @attr)
|
|
arguments: (argument_list (integer) @bits)) @match
|
|
(#eq? @mod "DSA") (#eq? @attr "generate") (#match? @bits "^(512|768|1024)$"))`,
|
|
},
|
|
},
|
|
sastRule{
|
|
Name: "py-ec-weak-curve",
|
|
Description: "`ec.generate_private_key(ec.SECT163K1())` / similar — < 224-bit elliptic curves are below NIST minimum. Use `SECP256R1` / `SECP384R1` / `SECP521R1` / `Curve25519`.",
|
|
Severity: "warning",
|
|
CWE: "CWE-326",
|
|
Tags: []string{"crypto", "weak-curve"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute attribute: (identifier) @curve)) @match
|
|
(#match? @curve "^(SECT163K1|SECT163R2|SECP160R1|SECP160R2|SECP160K1|SECP192R1|SECT193R1|SECT193R2)$"))`,
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
// 17. Mongo / Redis injection
|
|
func registerPythonMongoRedis() {
|
|
mustRegisterSAST(
|
|
sastRule{
|
|
Name: "py-mongo-where-injection",
|
|
Description: "Mongo `.find({\"$where\": user_input})` — `$where` runs JavaScript on the Mongo server; user-controlled JS is RCE on the DB host.",
|
|
Severity: "error",
|
|
CWE: "CWE-943",
|
|
Tags: []string{"nosql-injection", "mongodb"},
|
|
Pat: map[string]string{
|
|
"python": `((string) @match (#match? @match "[\"']\\$where[\"']"))`,
|
|
},
|
|
},
|
|
sastRule{
|
|
Name: "py-redis-eval-script",
|
|
Description: "Redis `.eval(script, ...)` / `.evalsha(...)` — Lua scripts run on the Redis server. User-controlled script content is RCE.",
|
|
Severity: "warning",
|
|
CWE: "CWE-943",
|
|
Tags: []string{"nosql-injection", "redis", "lua"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: (attribute attribute: (identifier) @fn)
|
|
arguments: (argument_list . (string) @script)) @match
|
|
(#match? @fn "^(eval|evalsha)$") (#match? @script "[\"']?[A-Z]+"))`,
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
// 18. Framework extras — Django middleware order, etc.
|
|
func registerPythonFrameworkExtras() {
|
|
mustRegisterSAST(
|
|
sastRule{
|
|
Name: "py-django-format-html-join-user-input",
|
|
Description: "Django `format_html_join(sep, fmt, args)` with user-controlled `args` — the format string is trusted but `sep` and elements still need escaping. Audit every site.",
|
|
Severity: "info",
|
|
CWE: "CWE-79",
|
|
Tags: []string{"xss", "django"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: [(identifier) @fn (attribute attribute: (identifier) @fn)]) @match (#eq? @fn "format_html_join"))`,
|
|
},
|
|
},
|
|
sastRule{
|
|
Name: "py-django-render-with-context-from-request",
|
|
Description: "Django `render(request, template, request.GET)` — passes the raw request mapping as template context. Any auto-escaping the template skips becomes user-controlled.",
|
|
Severity: "info",
|
|
CWE: "CWE-79",
|
|
Tags: []string{"xss", "django", "audit-hook"},
|
|
Pat: map[string]string{
|
|
"python": `((call function: [(identifier) @fn (attribute attribute: (identifier) @fn)]
|
|
arguments: (argument_list (_) (_) (attribute object: (identifier) @src))) @match
|
|
(#eq? @fn "render") (#eq? @src "request"))`,
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|