Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b910ed7
test(sarif): every rule must carry a title and a description
SizzleUnrlsd Oct 22, 2026
edf5ae2
feat(findings): describe every rule the analyzer reports
SizzleUnrlsd Oct 22, 2026
0459186
feat(sarif): describe each rule with its title and description
SizzleUnrlsd Oct 22, 2026
8dde45c
test(sarif): every result must carry its confidence
SizzleUnrlsd Oct 22, 2026
25a80bc
feat(sarif): give each result its confidence
SizzleUnrlsd Oct 22, 2026
ccc81d3
feat(findings): let a finding point at the other places it involves
SizzleUnrlsd Oct 22, 2026
ab094e2
test(sarif): a result must point at the other places its finding invo…
SizzleUnrlsd Oct 22, 2026
fbb4d4a
feat(sarif): write a finding's related locations
SizzleUnrlsd Oct 22, 2026
b8c198c
test(detectors): the sink a flow reaches inside a project function mu…
SizzleUnrlsd Oct 22, 2026
7203193
feat(cache): keep a finding's related locations
SizzleUnrlsd Oct 22, 2026
54ee868
feat(detectors): point at the sink a flow reaches inside a project fu…
SizzleUnrlsd Oct 22, 2026
0fc9803
docs(usage): describe the rules, the confidence and the related locat…
SizzleUnrlsd Oct 22, 2026
fc437e0
docs: announce the SARIF rule descriptions, confidence and related lo…
SizzleUnrlsd Oct 22, 2026
7637a58
test(sarif): a result's message must link to its related locations
SizzleUnrlsd Oct 22, 2026
c954739
test(detectors): the related location must not name the intermediary …
SizzleUnrlsd Oct 22, 2026
5835cd5
feat(sarif): link a result's message to its related locations
SizzleUnrlsd Oct 22, 2026
b3037d6
fix(detectors): say the sink is reached via the call, not in it
SizzleUnrlsd Oct 22, 2026
cda7457
test(plugins): a plugin must be able to describe the rules it reports
SizzleUnrlsd Oct 22, 2026
c2c7454
feat(plugins): let a plugin describe the rules it reports
SizzleUnrlsd Oct 22, 2026
5e8288f
feat(engine): carry the rule descriptions of a check to its report
SizzleUnrlsd Oct 22, 2026
d117b3c
refactor(plugins): describe each bundled rule in the plugin that repo…
SizzleUnrlsd Oct 22, 2026
38311e2
test(engine): every rule the code reports must be described, the engi…
SizzleUnrlsd Oct 22, 2026
8ef4204
feat(engine): describe the engine's own notes
SizzleUnrlsd Oct 22, 2026
8759356
docs(plugins): describe the rules attribute and related locations
SizzleUnrlsd Oct 22, 2026
04dfbd5
docs: say who describes a rule, and that the SARIF message links to r…
SizzleUnrlsd Oct 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## Unreleased

### Compatibility and migration

- The cache format changes (18): caches built by an earlier version miss once.
- SARIF: each rule carries a title (`shortDescription`) and a description (`fullDescription`)
instead of repeating its id (#270), and each result carries its confidence as the
`confidence` property (#271). A flow reaching its sink inside a project function points at
that sink as a related location, which the result's message links to (#271).
- Plugin API, additive: `Plugin.rules` maps the rules a plugin reports to their
`RuleDescription`, optional, so that third-party plugins describe their own rules;
`Finding.related` lists `RelatedLocation`s (a span and a message). `Report.rules` carries the
descriptions to the reporters.

## 0.23.0 (2026-10-16)

### Compatibility and migration
Expand Down
26 changes: 26 additions & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,32 @@ JSON pointer of a value) for a value of a structured file whose line cannot be:
that reads a finding's line checks which it has, and never invents a line for a
`FileLocation` (`coretrace_python.source.positions` places JSON and TOML values).

A plugin describes the rules it reports in its optional `rules` attribute: each rule id to a
`RuleDescription`, a title and what the rule detects. The SARIF report gives them to its
consumers (`shortDescription` and `fullDescription`), so that GitHub code scanning names and
explains the alert; a rule the plugin does not describe is listed by its id alone.

```python
from collections.abc import Mapping

from coretrace_python.findings import RuleDescription


class SensitiveLoggingPlugin(SymbolCallDetector):
...
rules: ClassVar[Mapping[str, RuleDescription]] = {
"sensitive-logging": RuleDescription(
"Sensitive data logged",
"A logging call at debug level that may write sensitive data to the logs.",
),
}
```

A finding may also point at other places it involves, each a `RelatedLocation` (a span and a
short message) in its `related` tuple, as a taint flow reaching its sink inside a project
function points at that sink. The SARIF report lists them as `relatedLocations` and links to
them from the message.

## Testing a plugin

Load the plugin directory with the engine and check one source or one project:
Expand Down
9 changes: 8 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,14 @@ several sources define the same advisory, it names the one whose definition is u
the next section describes.

`--format sarif` prints a SARIF 2.1.0 log, one run with the tool, its rules and one
result per finding. The root is declared once as the `SRCROOT` original URI base and
result per finding. Each rule carries a title (`shortDescription`) and what it detects
(`fullDescription`), so that a code scanning service can explain a finding: the plugin that
reports a rule describes it, the engine its own notes, and a rule a plugin does not describe
keeps its id alone. Each result carries the finding's confidence as the `confidence`
property. A flow that reaches its sink inside a project function is reported at the call, and
points at the sink as a related location (`relatedLocations`), with the message
`sink <sink>, reached via <function>`, which the result's message links to. The root is
declared once as the `SRCROOT` original URI base and
every location under it is relative to that base, which is what code scanning services
need to attach results to files. The components of a directory check are the tool's
`extensions`, their kind and digest in `properties`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,27 @@

from __future__ import annotations

from collections.abc import Sequence
from collections.abc import Mapping, Sequence
from typing import ClassVar

from coretrace_python.analysis import AnyAnalysis
from coretrace_python.dependency import DependencyAnalysis
from coretrace_python.findings import Confidence, Finding, Severity
from coretrace_python.findings import Confidence, Finding, RuleDescription, Severity
from coretrace_python.plugins import ProjectContext, ProjectPlugin


class DependencyPolicyPlugin(ProjectPlugin):
name: ClassVar[str] = "dependency-policy"
rules: ClassVar[Mapping[str, RuleDescription]] = {
"denied-dependency": RuleDescription(
"Denied dependency",
"A requirement on a package the policy denies.",
),
"unpinned-dependency": RuleDescription(
"Unpinned dependency",
"A requirement without an exact pin, when the policy requires pins.",
),
}
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis})

def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from __future__ import annotations

from collections.abc import Iterator, Sequence
from collections.abc import Iterator, Mapping, Sequence
from typing import ClassVar

from coretrace_python.analysis import AnyAnalysis
Expand All @@ -17,7 +17,7 @@
evidence,
suffix_index,
)
from coretrace_python.findings import Confidence, Finding
from coretrace_python.findings import Confidence, Finding, RuleDescription
from coretrace_python.interprocedural import CallGraphAnalysis, ExternalSymbol
from coretrace_python.plugins import ProjectContext, ProjectPlugin
from coretrace_python.semantic.symbols import SymbolId
Expand All @@ -26,6 +26,12 @@

class ReachableVulnerabilityPlugin(ProjectPlugin):
name: ClassVar[str] = "reachable-vulnerability"
rules: ClassVar[Mapping[str, RuleDescription]] = {
"reachable-vulnerability": RuleDescription(
"Reachable dependency vulnerability",
"The project calls an API that an advisory of a vulnerable requirement affects.",
),
}
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis, CallGraphAnalysis})

def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from collections.abc import Sequence
from collections.abc import Mapping, Sequence
from pathlib import PurePath
from typing import ClassVar

Expand All @@ -15,13 +15,19 @@
ruled_out,
suffix_index,
)
from coretrace_python.findings import Confidence, Finding
from coretrace_python.findings import Confidence, Finding, RuleDescription
from coretrace_python.interprocedural import CallGraphAnalysis, ExternalSymbol
from coretrace_python.plugins import ProjectContext, ProjectPlugin


class VulnerableDependencyPlugin(ProjectPlugin):
name: ClassVar[str] = "vulnerable-dependency"
rules: ClassVar[Mapping[str, RuleDescription]] = {
"vulnerable-dependency": RuleDescription(
"Vulnerable dependency",
"A requirement allows a version that an advisory affects.",
),
}
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis, CallGraphAnalysis})

def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@

from __future__ import annotations

from collections.abc import Mapping
from typing import ClassVar

from coretrace_python.abstract.strings import ModuleStringsAnalysis, leading_text
from coretrace_python.analysis import AnyAnalysis
from coretrace_python.findings import Severity
from coretrace_python.findings import RuleDescription, Severity
from coretrace_python.findings.refutation import Status, Verdict
from coretrace_python.hir import nodes
from coretrace_python.ir.ssa import SSAAnalysis
Expand All @@ -42,6 +43,12 @@

class CodeInjectionPlugin(TaintDetector):
name: ClassVar[str] = "code-injection"
rules: ClassVar[Mapping[str, RuleDescription]] = {
"code-injection": RuleDescription(
"Code injection",
"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.",
),
}
rule_id: ClassVar[str] = "code-injection"
kind: ClassVar[TaintKind] = TaintKind.CODE
severity: ClassVar[Severity] = Severity.HIGH
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

from coretrace_python.abstract.strings import ModuleStringsAnalysis, leading_text
from coretrace_python.analysis import AnyAnalysis
from coretrace_python.findings import Severity
from coretrace_python.findings import RuleDescription, Severity
from coretrace_python.findings.refutation import Status, Verdict
from coretrace_python.hir import nodes
from coretrace_python.interprocedural import Arguments
Expand Down Expand Up @@ -398,6 +398,12 @@ class _Operands:

class CommandInjectionPlugin(TaintDetector):
name: ClassVar[str] = "command-injection"
rules: ClassVar[Mapping[str, RuleDescription]] = {
"command-injection": RuleDescription(
"Command injection",
"Attacker-controlled data reaches a shell or process execution (os.system, subprocess, ...) where it can run or choose a command.",
),
}
rule_id: ClassVar[str] = "command-injection"
kind: ClassVar[TaintKind] = TaintKind.COMMAND
severity: ClassVar[Severity] = Severity.HIGH
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@

from __future__ import annotations

from collections.abc import Mapping
from typing import ClassVar

from coretrace_python.findings import Severity
from coretrace_python.findings import RuleDescription, Severity
from coretrace_python.plugins import TaintDetector
from coretrace_python.taint import TaintKind


class InsecureDeserializationPlugin(TaintDetector):
name: ClassVar[str] = "insecure-deserialization"
rules: ClassVar[Mapping[str, RuleDescription]] = {
"insecure-deserialization": RuleDescription(
"Insecure deserialization",
"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.",
),
}
rule_id: ClassVar[str] = "insecure-deserialization"
kind: ClassVar[TaintKind] = TaintKind.DESERIALIZATION
severity: ClassVar[Severity] = Severity.CRITICAL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@

from __future__ import annotations

from collections.abc import Mapping
from typing import ClassVar

from coretrace_python.findings import Severity
from coretrace_python.findings import RuleDescription, Severity
from coretrace_python.plugins import TaintDetector
from coretrace_python.taint import TaintKind


class NoSqlInjectionPlugin(TaintDetector):
name: ClassVar[str] = "nosql-injection"
rules: ClassVar[Mapping[str, RuleDescription]] = {
"nosql-injection": RuleDescription(
"NoSQL injection",
"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.",
),
}
rule_id: ClassVar[str] = "nosql-injection"
kind: ClassVar[TaintKind] = TaintKind.NOSQL
severity: ClassVar[Severity] = Severity.HIGH
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@

from __future__ import annotations

from collections.abc import Mapping
from typing import ClassVar

from coretrace_python.abstract.strings import ModuleStringsAnalysis
from coretrace_python.analysis import AnyAnalysis
from coretrace_python.findings import Severity
from coretrace_python.findings import RuleDescription, Severity
from coretrace_python.findings.refutation import Status, Verdict
from coretrace_python.hir import nodes
from coretrace_python.ir.ssa import SSAAnalysis
Expand All @@ -25,6 +26,12 @@

class OpenRedirectPlugin(TaintDetector):
name: ClassVar[str] = "open-redirect"
rules: ClassVar[Mapping[str, RuleDescription]] = {
"open-redirect": RuleDescription(
"Open redirect",
"Attacker-controlled data chooses the target of an HTTP redirect, which can send the user to another site.",
),
}
rule_id: ClassVar[str] = "open-redirect"
kind: ClassVar[TaintKind] = TaintKind.REDIRECT
severity: ClassVar[Severity] = Severity.MEDIUM
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@

from __future__ import annotations

from collections.abc import Mapping
from typing import ClassVar

from coretrace_python.findings import Severity
from coretrace_python.findings import RuleDescription, Severity
from coretrace_python.plugins import TaintDetector
from coretrace_python.taint import TaintKind


class PathTraversalPlugin(TaintDetector):
name: ClassVar[str] = "path-traversal"
rules: ClassVar[Mapping[str, RuleDescription]] = {
"path-traversal": RuleDescription(
"Path traversal",
"Attacker-controlled data reaches a file system path (open, send_file, os.remove, ...).",
),
}
rule_id: ClassVar[str] = "path-traversal"
kind: ClassVar[TaintKind] = TaintKind.PATH
severity: ClassVar[Severity] = Severity.HIGH
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@

from __future__ import annotations

from collections.abc import Mapping
from typing import ClassVar

from coretrace_python.findings import Confidence, Severity
from coretrace_python.findings import Confidence, RuleDescription, Severity
from coretrace_python.plugins import TaintDetector
from coretrace_python.taint import TaintKind


class PlaintextCredentialsPlugin(TaintDetector):
name: ClassVar[str] = "plaintext-credentials"
rules: ClassVar[Mapping[str, RuleDescription]] = {
"plaintext-credential-storage": RuleDescription(
"Plaintext credential storage",
"A parameter named like a password is stored in a database without hashing.",
),
}
rule_id: ClassVar[str] = "plaintext-credential-storage"
kind: ClassVar[TaintKind] = TaintKind.CREDENTIAL
severity: ClassVar[Severity] = Severity.HIGH
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@

from __future__ import annotations

from collections.abc import Mapping
from typing import ClassVar

from coretrace_python.findings import Severity
from coretrace_python.findings import RuleDescription, Severity
from coretrace_python.plugins import TaintDetector
from coretrace_python.taint import TaintKind


class SqlInjectionPlugin(TaintDetector):
name: ClassVar[str] = "sql-injection"
rules: ClassVar[Mapping[str, RuleDescription]] = {
"sql-injection": RuleDescription(
"SQL injection",
"Attacker-controlled data reaches a database statement (cursor.execute, SQLAlchemy text, Django raw, ...) rather than a query parameter.",
),
}
rule_id: ClassVar[str] = "sql-injection"
kind: ClassVar[TaintKind] = TaintKind.SQL
severity: ClassVar[Severity] = Severity.HIGH
Expand Down
9 changes: 8 additions & 1 deletion src/coretrace_python/bundled/security/ssrf/ssrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@

from __future__ import annotations

from collections.abc import Mapping
from typing import ClassVar

from coretrace_python.abstract.strings import ModuleStringsAnalysis
from coretrace_python.analysis import AnyAnalysis
from coretrace_python.findings import Severity
from coretrace_python.findings import RuleDescription, Severity
from coretrace_python.findings.refutation import Status, Verdict
from coretrace_python.hir import nodes
from coretrace_python.ir.ssa import SSAAnalysis
Expand All @@ -25,6 +26,12 @@

class SsrfPlugin(TaintDetector):
name: ClassVar[str] = "ssrf"
rules: ClassVar[Mapping[str, RuleDescription]] = {
"ssrf": RuleDescription(
"Server-side request forgery",
"Attacker-controlled data reaches the destination of an HTTP client request (Requests, httpx, urllib).",
),
}
rule_id: ClassVar[str] = "ssrf"
kind: ClassVar[TaintKind] = TaintKind.SSRF
severity: ClassVar[Severity] = Severity.HIGH
Expand Down
Loading
Loading