coretrace-python-analyzer 0.23.0__py3-none-any.whl → 0.25.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 (50) hide show
  1. coretrace_python/__init__.py +1 -1
  2. coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +12 -2
  3. coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +8 -2
  4. coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +8 -2
  5. coretrace_python/bundled/security/code_injection/code_injection.py +8 -1
  6. coretrace_python/bundled/security/command_injection/command_injection.py +7 -1
  7. coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +8 -1
  8. coretrace_python/bundled/security/nosql_injection/nosql_injection.py +8 -1
  9. coretrace_python/bundled/security/open_redirect/open_redirect.py +8 -1
  10. coretrace_python/bundled/security/path_traversal/path_traversal.py +8 -1
  11. coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +8 -1
  12. coretrace_python/bundled/security/sql_injection/sql_injection.py +8 -1
  13. coretrace_python/bundled/security/ssrf/ssrf.py +8 -1
  14. coretrace_python/bundled/security/xss/xss.py +8 -1
  15. coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +8 -1
  16. coretrace_python/bundled/syntax/flask_debug/flask_debug.py +8 -2
  17. coretrace_python/bundled/syntax/insecure_tempfile/insecure_tempfile.py +8 -1
  18. coretrace_python/bundled/syntax/insecure_tls/insecure_tls.py +7 -1
  19. coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +8 -2
  20. coretrace_python/bundled/syntax/unsafe_archive/unsafe_archive.py +7 -1
  21. coretrace_python/bundled/syntax/unsafe_xml/unsafe_xml.py +7 -1
  22. coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +8 -1
  23. coretrace_python/bundled/syntax/weak_random/weak_random.py +8 -2
  24. coretrace_python/cache.py +8 -2
  25. coretrace_python/cli.py +3 -1
  26. coretrace_python/engine.py +214 -88
  27. coretrace_python/findings/__init__.py +10 -1
  28. coretrace_python/findings/model.py +11 -1
  29. coretrace_python/findings/rules.py +16 -0
  30. coretrace_python/interprocedural/__init__.py +4 -0
  31. coretrace_python/interprocedural/modulegraph.py +54 -28
  32. coretrace_python/interprocedural/summaries.py +51 -15
  33. coretrace_python/plugins/api.py +5 -1
  34. coretrace_python/plugins/detectors.py +6 -1
  35. coretrace_python/plugins/secrets.py +16 -2
  36. coretrace_python/reporters/report.py +6 -2
  37. coretrace_python/reporters/sarif.py +57 -14
  38. coretrace_python/semantic/scopes.py +42 -1
  39. coretrace_python/source/__init__.py +17 -2
  40. coretrace_python/source/manager.py +74 -7
  41. coretrace_python/source/model.py +12 -1
  42. coretrace_python/source/positions.py +3 -20
  43. coretrace_python/taint/__init__.py +2 -0
  44. coretrace_python/taint/templates.py +901 -132
  45. {coretrace_python_analyzer-0.23.0.dist-info → coretrace_python_analyzer-0.25.0.dist-info}/METADATA +1 -1
  46. {coretrace_python_analyzer-0.23.0.dist-info → coretrace_python_analyzer-0.25.0.dist-info}/RECORD +50 -49
  47. {coretrace_python_analyzer-0.23.0.dist-info → coretrace_python_analyzer-0.25.0.dist-info}/WHEEL +1 -1
  48. {coretrace_python_analyzer-0.23.0.dist-info → coretrace_python_analyzer-0.25.0.dist-info}/entry_points.txt +0 -0
  49. {coretrace_python_analyzer-0.23.0.dist-info → coretrace_python_analyzer-0.25.0.dist-info}/licenses/LICENSE +0 -0
  50. {coretrace_python_analyzer-0.23.0.dist-info → coretrace_python_analyzer-0.25.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,4 +1,4 @@
1
1
  """CoreTrace's Python static analysis frontend."""
2
2
 
3
- __version__ = "0.23.0"
3
+ __version__ = "0.25.0"
4
4
 
@@ -2,17 +2,27 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Sequence
5
+ from collections.abc import Mapping, Sequence
6
6
  from typing import ClassVar
7
7
 
8
8
  from coretrace_python.analysis import AnyAnalysis
9
9
  from coretrace_python.dependency import DependencyAnalysis
10
- from coretrace_python.findings import Confidence, Finding, Severity
10
+ from coretrace_python.findings import Confidence, Finding, RuleDescription, Severity
11
11
  from coretrace_python.plugins import ProjectContext, ProjectPlugin
12
12
 
13
13
 
14
14
  class DependencyPolicyPlugin(ProjectPlugin):
15
15
  name: ClassVar[str] = "dependency-policy"
16
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
17
+ "denied-dependency": RuleDescription(
18
+ "Denied dependency",
19
+ "A requirement on a package the policy denies.",
20
+ ),
21
+ "unpinned-dependency": RuleDescription(
22
+ "Unpinned dependency",
23
+ "A requirement without an exact pin, when the policy requires pins.",
24
+ ),
25
+ }
16
26
  requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis})
17
27
 
18
28
  def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
@@ -4,7 +4,7 @@ template applies is a call to the function behind it, at the template's line."""
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- from collections.abc import Iterator, Sequence
7
+ from collections.abc import Iterator, Mapping, Sequence
8
8
  from typing import ClassVar
9
9
 
10
10
  from coretrace_python.analysis import AnyAnalysis
@@ -17,7 +17,7 @@ from coretrace_python.dependency.correlation import (
17
17
  evidence,
18
18
  suffix_index,
19
19
  )
20
- from coretrace_python.findings import Confidence, Finding
20
+ from coretrace_python.findings import Confidence, Finding, RuleDescription
21
21
  from coretrace_python.interprocedural import CallGraphAnalysis, ExternalSymbol
22
22
  from coretrace_python.plugins import ProjectContext, ProjectPlugin
23
23
  from coretrace_python.semantic.symbols import SymbolId
@@ -26,6 +26,12 @@ from coretrace_python.source import SourceSpan
26
26
 
27
27
  class ReachableVulnerabilityPlugin(ProjectPlugin):
28
28
  name: ClassVar[str] = "reachable-vulnerability"
29
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
30
+ "reachable-vulnerability": RuleDescription(
31
+ "Reachable dependency vulnerability",
32
+ "The project calls an API that an advisory of a vulnerable requirement affects.",
33
+ ),
34
+ }
29
35
  requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis, CallGraphAnalysis})
30
36
 
31
37
  def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Sequence
5
+ from collections.abc import Mapping, Sequence
6
6
  from pathlib import PurePath
7
7
  from typing import ClassVar
8
8
 
@@ -15,13 +15,19 @@ from coretrace_python.dependency.correlation import (
15
15
  ruled_out,
16
16
  suffix_index,
17
17
  )
18
- from coretrace_python.findings import Confidence, Finding
18
+ from coretrace_python.findings import Confidence, Finding, RuleDescription
19
19
  from coretrace_python.interprocedural import CallGraphAnalysis, ExternalSymbol
20
20
  from coretrace_python.plugins import ProjectContext, ProjectPlugin
21
21
 
22
22
 
23
23
  class VulnerableDependencyPlugin(ProjectPlugin):
24
24
  name: ClassVar[str] = "vulnerable-dependency"
25
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
26
+ "vulnerable-dependency": RuleDescription(
27
+ "Vulnerable dependency",
28
+ "A requirement allows a version that an advisory affects.",
29
+ ),
30
+ }
25
31
  requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis, CallGraphAnalysis})
26
32
 
27
33
  def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
@@ -18,11 +18,12 @@ runs depends on what the object exposes, a hotspot."""
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ from collections.abc import Mapping
21
22
  from typing import ClassVar
22
23
 
23
24
  from coretrace_python.abstract.strings import ModuleStringsAnalysis, leading_text
24
25
  from coretrace_python.analysis import AnyAnalysis
25
- from coretrace_python.findings import Severity
26
+ from coretrace_python.findings import RuleDescription, Severity
26
27
  from coretrace_python.findings.refutation import Status, Verdict
27
28
  from coretrace_python.hir import nodes
28
29
  from coretrace_python.ir.ssa import SSAAnalysis
@@ -42,6 +43,12 @@ _GETATTR = SymbolId("python.builtins.getattr")
42
43
 
43
44
  class CodeInjectionPlugin(TaintDetector):
44
45
  name: ClassVar[str] = "code-injection"
46
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
47
+ "code-injection": RuleDescription(
48
+ "Code injection",
49
+ "Attacker-controlled data chooses code that runs: the code eval or exec runs, the module an import loads, the script runpy runs, or the attribute getattr selects and the program calls.",
50
+ ),
51
+ }
45
52
  rule_id: ClassVar[str] = "code-injection"
46
53
  kind: ClassVar[TaintKind] = TaintKind.CODE
47
54
  severity: ClassVar[Severity] = Severity.HIGH
@@ -36,7 +36,7 @@ from typing import ClassVar
36
36
 
37
37
  from coretrace_python.abstract.strings import ModuleStringsAnalysis, leading_text
38
38
  from coretrace_python.analysis import AnyAnalysis
39
- from coretrace_python.findings import Severity
39
+ from coretrace_python.findings import RuleDescription, Severity
40
40
  from coretrace_python.findings.refutation import Status, Verdict
41
41
  from coretrace_python.hir import nodes
42
42
  from coretrace_python.interprocedural import Arguments
@@ -398,6 +398,12 @@ class _Operands:
398
398
 
399
399
  class CommandInjectionPlugin(TaintDetector):
400
400
  name: ClassVar[str] = "command-injection"
401
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
402
+ "command-injection": RuleDescription(
403
+ "Command injection",
404
+ "Attacker-controlled data reaches a shell or process execution (os.system, subprocess, ...) where it can run or choose a command.",
405
+ ),
406
+ }
401
407
  rule_id: ClassVar[str] = "command-injection"
402
408
  kind: ClassVar[TaintKind] = TaintKind.COMMAND
403
409
  severity: ClassVar[Severity] = Severity.HIGH
@@ -2,15 +2,22 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Mapping
5
6
  from typing import ClassVar
6
7
 
7
- from coretrace_python.findings import Severity
8
+ from coretrace_python.findings import RuleDescription, Severity
8
9
  from coretrace_python.plugins import TaintDetector
9
10
  from coretrace_python.taint import TaintKind
10
11
 
11
12
 
12
13
  class InsecureDeserializationPlugin(TaintDetector):
13
14
  name: ClassVar[str] = "insecure-deserialization"
15
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
16
+ "insecure-deserialization": RuleDescription(
17
+ "Insecure deserialization",
18
+ "Attacker-controlled data reaches a deserializer that can build arbitrary objects or run code: pickle, marshal, dill, jsonpickle, shelve, or a YAML loader without a safe loader.",
19
+ ),
20
+ }
14
21
  rule_id: ClassVar[str] = "insecure-deserialization"
15
22
  kind: ClassVar[TaintKind] = TaintKind.DESERIALIZATION
16
23
  severity: ClassVar[Severity] = Severity.CRITICAL
@@ -5,15 +5,22 @@ sink; model plugins declare them."""
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ from collections.abc import Mapping
8
9
  from typing import ClassVar
9
10
 
10
- from coretrace_python.findings import Severity
11
+ from coretrace_python.findings import RuleDescription, Severity
11
12
  from coretrace_python.plugins import TaintDetector
12
13
  from coretrace_python.taint import TaintKind
13
14
 
14
15
 
15
16
  class NoSqlInjectionPlugin(TaintDetector):
16
17
  name: ClassVar[str] = "nosql-injection"
18
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
19
+ "nosql-injection": RuleDescription(
20
+ "NoSQL injection",
21
+ "Attacker input that may hold a structure, such as a JSON body, reaches a NoSQL query, where an operator such as {\"$ne\": null} matches any value.",
22
+ ),
23
+ }
17
24
  rule_id: ClassVar[str] = "nosql-injection"
18
25
  kind: ClassVar[TaintKind] = TaintKind.NOSQL
19
26
  severity: ClassVar[Severity] = Severity.HIGH
@@ -10,11 +10,12 @@ unproven, ``"/" + next`` included, stays a vulnerability.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ from collections.abc import Mapping
13
14
  from typing import ClassVar
14
15
 
15
16
  from coretrace_python.abstract.strings import ModuleStringsAnalysis
16
17
  from coretrace_python.analysis import AnyAnalysis
17
- from coretrace_python.findings import Severity
18
+ from coretrace_python.findings import RuleDescription, Severity
18
19
  from coretrace_python.findings.refutation import Status, Verdict
19
20
  from coretrace_python.hir import nodes
20
21
  from coretrace_python.ir.ssa import SSAAnalysis
@@ -25,6 +26,12 @@ from coretrace_python.taint.urls import flow_url
25
26
 
26
27
  class OpenRedirectPlugin(TaintDetector):
27
28
  name: ClassVar[str] = "open-redirect"
29
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
30
+ "open-redirect": RuleDescription(
31
+ "Open redirect",
32
+ "Attacker-controlled data chooses the target of an HTTP redirect, which can send the user to another site.",
33
+ ),
34
+ }
28
35
  rule_id: ClassVar[str] = "open-redirect"
29
36
  kind: ClassVar[TaintKind] = TaintKind.REDIRECT
30
37
  severity: ClassVar[Severity] = Severity.MEDIUM
@@ -2,15 +2,22 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Mapping
5
6
  from typing import ClassVar
6
7
 
7
- from coretrace_python.findings import Severity
8
+ from coretrace_python.findings import RuleDescription, Severity
8
9
  from coretrace_python.plugins import TaintDetector
9
10
  from coretrace_python.taint import TaintKind
10
11
 
11
12
 
12
13
  class PathTraversalPlugin(TaintDetector):
13
14
  name: ClassVar[str] = "path-traversal"
15
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
16
+ "path-traversal": RuleDescription(
17
+ "Path traversal",
18
+ "Attacker-controlled data reaches a file system path (open, send_file, os.remove, ...).",
19
+ ),
20
+ }
14
21
  rule_id: ClassVar[str] = "path-traversal"
15
22
  kind: ClassVar[TaintKind] = TaintKind.PATH
16
23
  severity: ClassVar[Severity] = Severity.HIGH
@@ -5,15 +5,22 @@ a hint, so medium confidence."""
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ from collections.abc import Mapping
8
9
  from typing import ClassVar
9
10
 
10
- from coretrace_python.findings import Confidence, Severity
11
+ from coretrace_python.findings import Confidence, RuleDescription, Severity
11
12
  from coretrace_python.plugins import TaintDetector
12
13
  from coretrace_python.taint import TaintKind
13
14
 
14
15
 
15
16
  class PlaintextCredentialsPlugin(TaintDetector):
16
17
  name: ClassVar[str] = "plaintext-credentials"
18
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
19
+ "plaintext-credential-storage": RuleDescription(
20
+ "Plaintext credential storage",
21
+ "A parameter named like a password is stored in a database without hashing.",
22
+ ),
23
+ }
17
24
  rule_id: ClassVar[str] = "plaintext-credential-storage"
18
25
  kind: ClassVar[TaintKind] = TaintKind.CREDENTIAL
19
26
  severity: ClassVar[Severity] = Severity.HIGH
@@ -2,15 +2,22 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Mapping
5
6
  from typing import ClassVar
6
7
 
7
- from coretrace_python.findings import Severity
8
+ from coretrace_python.findings import RuleDescription, Severity
8
9
  from coretrace_python.plugins import TaintDetector
9
10
  from coretrace_python.taint import TaintKind
10
11
 
11
12
 
12
13
  class SqlInjectionPlugin(TaintDetector):
13
14
  name: ClassVar[str] = "sql-injection"
15
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
16
+ "sql-injection": RuleDescription(
17
+ "SQL injection",
18
+ "Attacker-controlled data reaches a database statement (cursor.execute, SQLAlchemy text, Django raw, ...) rather than a query parameter.",
19
+ ),
20
+ }
14
21
  rule_id: ClassVar[str] = "sql-injection"
15
22
  kind: ClassVar[TaintKind] = TaintKind.SQL
16
23
  severity: ClassVar[Severity] = Severity.HIGH
@@ -10,11 +10,12 @@ a base of unknown value or a URL built in a helper function, stays a vulnerabili
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ from collections.abc import Mapping
13
14
  from typing import ClassVar
14
15
 
15
16
  from coretrace_python.abstract.strings import ModuleStringsAnalysis
16
17
  from coretrace_python.analysis import AnyAnalysis
17
- from coretrace_python.findings import Severity
18
+ from coretrace_python.findings import RuleDescription, Severity
18
19
  from coretrace_python.findings.refutation import Status, Verdict
19
20
  from coretrace_python.hir import nodes
20
21
  from coretrace_python.ir.ssa import SSAAnalysis
@@ -25,6 +26,12 @@ from coretrace_python.taint.urls import flow_url, redirects_disabled
25
26
 
26
27
  class SsrfPlugin(TaintDetector):
27
28
  name: ClassVar[str] = "ssrf"
29
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
30
+ "ssrf": RuleDescription(
31
+ "Server-side request forgery",
32
+ "Attacker-controlled data reaches the destination of an HTTP client request (Requests, httpx, urllib).",
33
+ ),
34
+ }
28
35
  rule_id: ClassVar[str] = "ssrf"
29
36
  kind: ClassVar[TaintKind] = TaintKind.SSRF
30
37
  severity: ClassVar[Severity] = Severity.HIGH
@@ -2,15 +2,22 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Mapping
5
6
  from typing import ClassVar
6
7
 
7
- from coretrace_python.findings import Severity
8
+ from coretrace_python.findings import RuleDescription, Severity
8
9
  from coretrace_python.plugins import TaintDetector
9
10
  from coretrace_python.taint import TaintKind
10
11
 
11
12
 
12
13
  class XssPlugin(TaintDetector):
13
14
  name: ClassVar[str] = "xss"
15
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
16
+ "xss": RuleDescription(
17
+ "Cross-site scripting",
18
+ "Attacker-controlled data reaches an HTTP response body built without escaping.",
19
+ ),
20
+ }
14
21
  rule_id: ClassVar[str] = "xss"
15
22
  kind: ClassVar[TaintKind] = TaintKind.HTML
16
23
  severity: ClassVar[Severity] = Severity.HIGH
@@ -2,15 +2,22 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Mapping
5
6
  from typing import ClassVar
6
7
 
7
- from coretrace_python.findings import Severity
8
+ from coretrace_python.findings import RuleDescription, Severity
8
9
  from coretrace_python.plugins import SymbolCallDetector
9
10
  from coretrace_python.semantic.symbols import SymbolId
10
11
 
11
12
 
12
13
  class DangerousEvalPlugin(SymbolCallDetector):
13
14
  name: ClassVar[str] = "dangerous-eval"
15
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
16
+ "dangerous-eval": RuleDescription(
17
+ "Dynamic code execution",
18
+ "A call to eval or exec, whatever name the file gives them.",
19
+ ),
20
+ }
14
21
  rule_id: ClassVar[str] = "dangerous-eval"
15
22
  symbols: ClassVar[frozenset[SymbolId]] = frozenset(
16
23
  {SymbolId("python.builtins.eval"), SymbolId("python.builtins.exec")}
@@ -2,11 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Iterator, Sequence
5
+ from collections.abc import Iterator, Mapping, Sequence
6
6
  from typing import ClassVar
7
7
 
8
8
  from coretrace_python.analysis import AnyAnalysis
9
- from coretrace_python.findings import Confidence, Finding, Severity
9
+ from coretrace_python.findings import Confidence, Finding, RuleDescription, Severity
10
10
  from coretrace_python.hir import nodes
11
11
  from coretrace_python.hir.visitors import Node, children
12
12
  from coretrace_python.plugins import Plugin, PluginContext
@@ -18,6 +18,12 @@ RUN = SymbolId("python.flask.Flask.run")
18
18
 
19
19
  class FlaskDebugPlugin(Plugin):
20
20
  name: ClassVar[str] = "flask-debug"
21
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
22
+ "debug-enabled": RuleDescription(
23
+ "Debug mode enabled",
24
+ "A Flask application run with debug=True, which exposes an interactive debugger.",
25
+ ),
26
+ }
21
27
  requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({ScopeAnalysis, SymbolAnalysis})
22
28
 
23
29
  def analyze(self, ctx: PluginContext) -> Sequence[Finding]:
@@ -3,10 +3,11 @@ create it first. ``mkstemp``, ``NamedTemporaryFile`` and ``TemporaryDirectory``
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ from collections.abc import Mapping
6
7
  from typing import ClassVar
7
8
 
8
9
  from coretrace_python.analysis import AnyAnalysis
9
- from coretrace_python.findings import Severity
10
+ from coretrace_python.findings import RuleDescription, Severity
10
11
  from coretrace_python.interprocedural import CallGraphAnalysis
11
12
  from coretrace_python.ir.ssa import SSAAnalysis
12
13
  from coretrace_python.plugins import SymbolCallDetector
@@ -15,6 +16,12 @@ from coretrace_python.semantic.symbols import SymbolId
15
16
 
16
17
  class InsecureTempFilePlugin(SymbolCallDetector):
17
18
  name: ClassVar[str] = "insecure-temp-file"
19
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
20
+ "insecure-temp-file": RuleDescription(
21
+ "Insecure temporary file",
22
+ "tempfile.mktemp, which names a file without creating it, so that another process can create it first.",
23
+ ),
24
+ }
18
25
  rule_id: ClassVar[str] = "insecure-temp-file"
19
26
  requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis, CallGraphAnalysis})
20
27
  symbols: ClassVar[frozenset[SymbolId]] = frozenset({SymbolId("python.tempfile.mktemp")})
@@ -7,7 +7,7 @@ from collections.abc import Mapping
7
7
  from typing import ClassVar
8
8
 
9
9
  from coretrace_python.analysis import AnyAnalysis
10
- from coretrace_python.findings import Severity
10
+ from coretrace_python.findings import RuleDescription, Severity
11
11
  from coretrace_python.interprocedural import CallGraphAnalysis
12
12
  from coretrace_python.ir.model import Call, Constant, Instruction, Value
13
13
  from coretrace_python.ir.ssa import SSAAnalysis
@@ -24,6 +24,12 @@ UNVERIFIED = frozenset({SymbolId("python.ssl._create_unverified_context")})
24
24
 
25
25
  class InsecureTlsPlugin(SymbolCallDetector):
26
26
  name: ClassVar[str] = "insecure-tls"
27
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
28
+ "insecure-tls": RuleDescription(
29
+ "TLS certificate verification disabled",
30
+ "verify=False on a Requests or httpx call, or ssl._create_unverified_context(), which accept any certificate.",
31
+ ),
32
+ }
27
33
  rule_id: ClassVar[str] = "insecure-tls"
28
34
  requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis, CallGraphAnalysis})
29
35
  symbols: ClassVar[frozenset[SymbolId]] = HTTP_CALLS | UNVERIFIED
@@ -2,11 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Sequence
5
+ from collections.abc import Mapping, Sequence
6
6
  from typing import ClassVar
7
7
 
8
8
  from coretrace_python.analysis import AnyAnalysis
9
- from coretrace_python.findings import Confidence, Finding, Severity
9
+ from coretrace_python.findings import Confidence, Finding, RuleDescription, Severity
10
10
  from coretrace_python.interprocedural import CallGraphAnalysis
11
11
  from coretrace_python.ir.model import Call
12
12
  from coretrace_python.ir.ssa import SSAAnalysis
@@ -20,6 +20,12 @@ HTTP_CALLS = frozenset(SymbolId(f"python.{caller}.{method}") for caller in _CALL
20
20
 
21
21
  class MissingTimeoutPlugin(Plugin):
22
22
  name: ClassVar[str] = "missing-timeout"
23
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
24
+ "missing-timeout": RuleDescription(
25
+ "HTTP request without a timeout",
26
+ "A Requests or httpx call without a timeout, which can wait forever.",
27
+ ),
28
+ }
23
29
  requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis, CallGraphAnalysis})
24
30
 
25
31
  def analyze(self, ctx: PluginContext) -> Sequence[Finding]:
@@ -7,7 +7,7 @@ from collections.abc import Mapping
7
7
  from typing import ClassVar
8
8
 
9
9
  from coretrace_python.analysis import AnyAnalysis
10
- from coretrace_python.findings import Severity
10
+ from coretrace_python.findings import RuleDescription, Severity
11
11
  from coretrace_python.interprocedural import CallGraphAnalysis
12
12
  from coretrace_python.ir.model import Call, Instruction, Value
13
13
  from coretrace_python.ir.ssa import SSAAnalysis
@@ -20,6 +20,12 @@ _EXTRACTORS = tuple(f"{opener}.{method}" for opener in _OPENERS for method in ("
20
20
 
21
21
  class UnsafeArchivePlugin(SymbolCallDetector):
22
22
  name: ClassVar[str] = "unsafe-archive-extraction"
23
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
24
+ "unsafe-archive-extraction": RuleDescription(
25
+ "Unsafe archive extraction",
26
+ "A tarfile or zipfile extraction, or shutil.unpack_archive, without a member filter: a member can be written outside the destination.",
27
+ ),
28
+ }
23
29
  rule_id: ClassVar[str] = "unsafe-archive-extraction"
24
30
  requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis, CallGraphAnalysis})
25
31
  symbols: ClassVar[frozenset[SymbolId]] = frozenset(
@@ -19,7 +19,7 @@ from typing import ClassVar
19
19
 
20
20
  from coretrace_python.abstract.strings import ModuleStringsAnalysis
21
21
  from coretrace_python.analysis import AnyAnalysis
22
- from coretrace_python.findings import Confidence, Severity
22
+ from coretrace_python.findings import Confidence, RuleDescription, Severity
23
23
  from coretrace_python.interprocedural import CallGraphAnalysis
24
24
  from coretrace_python.ir.model import Call, Constant, Global, Instruction, Value
25
25
  from coretrace_python.ir.ssa import SSAAnalysis
@@ -64,6 +64,12 @@ def _resolve_entities(call: Call, defs: Mapping[Value, Instruction]) -> object:
64
64
 
65
65
  class UnsafeXmlPlugin(SymbolCallDetector):
66
66
  name: ClassVar[str] = "unsafe-xml"
67
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
68
+ "unsafe-xml": RuleDescription(
69
+ "Unsafe XML parser",
70
+ "An XML parser of the standard library or of lxml, exposed to the entities a document declares; defusedxml is the safe replacement.",
71
+ ),
72
+ }
67
73
  rule_id: ClassVar[str] = "unsafe-xml"
68
74
  requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis, CallGraphAnalysis, ModuleStringsAnalysis})
69
75
  symbols: ClassVar[frozenset[SymbolId]] = _STANDARD_LIBRARY | _LXML | _LXML_UNTIL_6_1
@@ -2,15 +2,22 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Mapping
5
6
  from typing import ClassVar
6
7
 
7
- from coretrace_python.findings import Severity
8
+ from coretrace_python.findings import RuleDescription, Severity
8
9
  from coretrace_python.plugins import SymbolCallDetector
9
10
  from coretrace_python.semantic.symbols import SymbolId
10
11
 
11
12
 
12
13
  class WeakCryptoPlugin(SymbolCallDetector):
13
14
  name: ClassVar[str] = "weak-crypto"
15
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
16
+ "weak-crypto": RuleDescription(
17
+ "Weak cryptographic algorithm",
18
+ "A broken algorithm such as hashlib.md5 or hashlib.sha1.",
19
+ ),
20
+ }
14
21
  rule_id: ClassVar[str] = "weak-crypto"
15
22
  symbols: ClassVar[frozenset[SymbolId]] = frozenset(
16
23
  {SymbolId("python.hashlib.md5"), SymbolId("python.hashlib.sha1")}
@@ -7,11 +7,11 @@ produces one."""
7
7
  from __future__ import annotations
8
8
 
9
9
  import re
10
- from collections.abc import Iterator, Sequence
10
+ from collections.abc import Iterator, Mapping, Sequence
11
11
  from typing import ClassVar
12
12
 
13
13
  from coretrace_python.analysis import AnyAnalysis
14
- from coretrace_python.findings import Confidence, Finding, Severity
14
+ from coretrace_python.findings import Confidence, Finding, RuleDescription, Severity
15
15
  from coretrace_python.hir import nodes
16
16
  from coretrace_python.hir.visitors import Node, children
17
17
  from coretrace_python.plugins import Plugin, PluginContext
@@ -25,6 +25,12 @@ CREDENTIAL = re.compile(r"(?i)(token|secret|password|passwd|api_?key|session|non
25
25
 
26
26
  class WeakRandomPlugin(Plugin):
27
27
  name: ClassVar[str] = "weak-random"
28
+ rules: ClassVar[Mapping[str, RuleDescription]] = {
29
+ "weak-random": RuleDescription(
30
+ "Weak random value for a credential",
31
+ "A value of the random module used as a credential; random is not cryptographically secure.",
32
+ ),
33
+ }
28
34
  rule_id: ClassVar[str] = "weak-random"
29
35
  requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({ScopeAnalysis, SymbolAnalysis})
30
36
 
coretrace_python/cache.py CHANGED
@@ -21,7 +21,7 @@ from pathlib import Path
21
21
  from types import MappingProxyType
22
22
  from typing import Any
23
23
 
24
- from coretrace_python.findings import Confidence, Finding, Severity
24
+ from coretrace_python.findings import Confidence, Finding, RelatedLocation, Severity
25
25
  from coretrace_python.interprocedural import (
26
26
  Arguments,
27
27
  CallSite,
@@ -42,7 +42,11 @@ from coretrace_python.interprocedural import (
42
42
  from coretrace_python.semantic.symbols import SymbolId
43
43
  from coretrace_python.source import FileLocation, Location, SourceId, SourceSpan
44
44
 
45
- CACHE_FORMAT = 16
45
+ # Cached summaries from earlier versions preserve unstable external-call
46
+ # insertion order. They must be recomputed with canonical ordering (#261).
47
+ # Findings carry their related locations from format 18 on (#271). A module's key holds
48
+ # the names it is imported by from format 19 on (#264).
49
+ CACHE_FORMAT = 19
46
50
 
47
51
 
48
52
  @dataclass(frozen=True)
@@ -227,6 +231,7 @@ def _encode_finding(finding: Finding) -> dict[str, Any]:
227
231
  "span": _encode_location(finding.span),
228
232
  "function": finding.function,
229
233
  "metadata": dict(finding.metadata),
234
+ "related": [[_encode_span(r.span), r.message] for r in finding.related],
230
235
  }
231
236
 
232
237
 
@@ -240,6 +245,7 @@ def _decode_finding(data: Mapping[str, Any]) -> Finding:
240
245
  _decode_location(data["span"]),
241
246
  None if function is None else _string(function),
242
247
  {_string(k): _string(v) for k, v in data["metadata"].items()},
248
+ tuple(RelatedLocation(_decode_span(span), _string(message)) for span, message in data["related"]),
243
249
  )
244
250
 
245
251
 
coretrace_python/cli.py CHANGED
@@ -280,6 +280,7 @@ def main(argv: list[str] | None = None) -> int:
280
280
  coverage = analysis.coverage
281
281
  suppressed = analysis.suppressed
282
282
  components = analysis.components
283
+ rules = analysis.rules
283
284
  if args.sbom is not None:
284
285
  args.sbom.write_text(
285
286
  render_sbom(
@@ -307,6 +308,7 @@ def main(argv: list[str] | None = None) -> int:
307
308
  findings, coverage = file_analysis.findings, file_analysis.coverage
308
309
  suppressed = file_analysis.suppressed
309
310
  components = ()
311
+ rules = file_analysis.rules
310
312
  baselined = None
311
313
  recorded = None
312
314
  if previous is not None:
@@ -317,7 +319,7 @@ def main(argv: list[str] | None = None) -> int:
317
319
  elif args.baseline is not None:
318
320
  recorded = Baseline.of(findings, root, key)
319
321
  findings, baselined = (), findings
320
- report = engine.report(findings, coverage, root, suppressed, baselined, components)
322
+ report = engine.report(findings, coverage, root, suppressed, baselined, components, rules)
321
323
  text = render(args.format or "text", report)
322
324
  # Recorded once the report is rendered, so that a failed check keeps the
323
325
  # previous baseline.