coretrace-python-analyzer 0.1.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.
- coretrace_python/__init__.py +4 -0
- coretrace_python/__main__.py +4 -0
- coretrace_python/abstract/__init__.py +45 -0
- coretrace_python/abstract/constants.py +226 -0
- coretrace_python/abstract/heap.py +252 -0
- coretrace_python/abstract/ranges.py +285 -0
- coretrace_python/abstract/values.py +56 -0
- coretrace_python/analysis/__init__.py +31 -0
- coretrace_python/analysis/manager.py +165 -0
- coretrace_python/analysis/provider.py +73 -0
- coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +46 -0
- coretrace_python/bundled/dependency/dependency_policy/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +56 -0
- coretrace_python/bundled/dependency/sample_advisories/plugin.toml +9 -0
- coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +109 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +9 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +43 -0
- coretrace_python/bundled/models/cli/cli_models.py +35 -0
- coretrace_python/bundled/models/cli/plugin.toml +9 -0
- coretrace_python/bundled/models/credentials/credential_models.py +43 -0
- coretrace_python/bundled/models/credentials/plugin.toml +9 -0
- coretrace_python/bundled/models/django/django_models.py +123 -0
- coretrace_python/bundled/models/django/plugin.toml +9 -0
- coretrace_python/bundled/models/fastapi/fastapi_models.py +29 -0
- coretrace_python/bundled/models/fastapi/plugin.toml +9 -0
- coretrace_python/bundled/models/flask/flask_models.py +53 -0
- coretrace_python/bundled/models/flask/plugin.toml +9 -0
- coretrace_python/bundled/models/http_clients/http_client_models.py +43 -0
- coretrace_python/bundled/models/http_clients/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/python_stdlib.py +68 -0
- coretrace_python/bundled/models/sqlalchemy/plugin.toml +9 -0
- coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +47 -0
- coretrace_python/bundled/secrets/config_secrets/config_secrets.py +38 -0
- coretrace_python/bundled/secrets/config_secrets/plugin.toml +9 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +19 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +9 -0
- coretrace_python/bundled/security/command_injection/command_injection.py +17 -0
- coretrace_python/bundled/security/command_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +17 -0
- coretrace_python/bundled/security/insecure_deserialization/plugin.toml +9 -0
- coretrace_python/bundled/security/open_redirect/open_redirect.py +17 -0
- coretrace_python/bundled/security/open_redirect/plugin.toml +9 -0
- coretrace_python/bundled/security/path_traversal/path_traversal.py +17 -0
- coretrace_python/bundled/security/path_traversal/plugin.toml +9 -0
- coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +21 -0
- coretrace_python/bundled/security/plaintext_credentials/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/sql_injection.py +17 -0
- coretrace_python/bundled/security/ssrf/plugin.toml +9 -0
- coretrace_python/bundled/security/ssrf/ssrf.py +17 -0
- coretrace_python/bundled/security/xss/plugin.toml +9 -0
- coretrace_python/bundled/security/xss/xss.py +17 -0
- coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +19 -0
- coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +9 -0
- coretrace_python/bundled/syntax/flask_debug/flask_debug.py +63 -0
- coretrace_python/bundled/syntax/flask_debug/plugin.toml +9 -0
- coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +51 -0
- coretrace_python/bundled/syntax/missing_timeout/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +19 -0
- coretrace_python/cache.py +310 -0
- coretrace_python/cfg/__init__.py +46 -0
- coretrace_python/cfg/builder.py +589 -0
- coretrace_python/cfg/dominance.py +183 -0
- coretrace_python/cfg/model.py +166 -0
- coretrace_python/cli.py +216 -0
- coretrace_python/dataflow/__init__.py +29 -0
- coretrace_python/dataflow/lattice.py +78 -0
- coretrace_python/dataflow/solver.py +96 -0
- coretrace_python/dependency/__init__.py +44 -0
- coretrace_python/dependency/advisories.py +168 -0
- coretrace_python/dependency/correlation.py +87 -0
- coretrace_python/dependency/graph.py +274 -0
- coretrace_python/dependency/policy.py +65 -0
- coretrace_python/dependency/sbom.py +65 -0
- coretrace_python/engine.py +688 -0
- coretrace_python/findings/__init__.py +13 -0
- coretrace_python/findings/coverage.py +45 -0
- coretrace_python/findings/model.py +49 -0
- coretrace_python/findings/refutation.py +427 -0
- coretrace_python/frontend/__init__.py +18 -0
- coretrace_python/frontend/ast_adapter.py +447 -0
- coretrace_python/frontend/parser.py +23 -0
- coretrace_python/hir/__init__.py +5 -0
- coretrace_python/hir/nodes.py +584 -0
- coretrace_python/hir/visitors.py +36 -0
- coretrace_python/interprocedural/__init__.py +49 -0
- coretrace_python/interprocedural/callgraph.py +291 -0
- coretrace_python/interprocedural/modulegraph.py +218 -0
- coretrace_python/interprocedural/summaries.py +463 -0
- coretrace_python/ir/__init__.py +5 -0
- coretrace_python/ir/defuse.py +74 -0
- coretrace_python/ir/lowering.py +575 -0
- coretrace_python/ir/model.py +481 -0
- coretrace_python/ir/printer.py +201 -0
- coretrace_python/ir/ssa.py +277 -0
- coretrace_python/plugins/__init__.py +58 -0
- coretrace_python/plugins/api.py +153 -0
- coretrace_python/plugins/detectors.py +114 -0
- coretrace_python/plugins/loader.py +84 -0
- coretrace_python/plugins/manifest.py +112 -0
- coretrace_python/plugins/registry.py +34 -0
- coretrace_python/plugins/secrets.py +330 -0
- coretrace_python/reporters/__init__.py +22 -0
- coretrace_python/reporters/json_format.py +48 -0
- coretrace_python/reporters/report.py +23 -0
- coretrace_python/reporters/sarif.py +70 -0
- coretrace_python/reporters/text.py +24 -0
- coretrace_python/semantic/__init__.py +9 -0
- coretrace_python/semantic/identity.py +39 -0
- coretrace_python/semantic/imports.py +131 -0
- coretrace_python/semantic/scopes.py +473 -0
- coretrace_python/semantic/symbols.py +87 -0
- coretrace_python/source/__init__.py +13 -0
- coretrace_python/source/manager.py +81 -0
- coretrace_python/source/model.py +57 -0
- coretrace_python/taint/__init__.py +55 -0
- coretrace_python/taint/engine.py +800 -0
- coretrace_python/taint/models.py +317 -0
- coretrace_python/taint/routes.py +99 -0
- coretrace_python_analyzer-0.1.0.dist-info/METADATA +74 -0
- coretrace_python_analyzer-0.1.0.dist-info/RECORD +126 -0
- coretrace_python_analyzer-0.1.0.dist-info/WHEEL +4 -0
- coretrace_python_analyzer-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Security models for the Python standard library (architecture §16)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from coretrace_python.plugins import ModelPlugin
|
|
8
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
9
|
+
from coretrace_python.taint import Model, Sanitizer, Sink, Source, TaintKind, Validator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _sym(path: str) -> SymbolId:
|
|
13
|
+
return SymbolId(f"python.{path}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PythonStdlibModels(ModelPlugin):
|
|
17
|
+
name: ClassVar[str] = "python-stdlib-models"
|
|
18
|
+
models: ClassVar[tuple[Model, ...]] = (
|
|
19
|
+
Source(_sym("builtins.input"), "stdin"),
|
|
20
|
+
Source(_sym("sys.stdin"), "stdin"),
|
|
21
|
+
# A command-line tool is expected to open the paths it is given.
|
|
22
|
+
Source(_sym("sys.argv"), "argv", TaintKind.ALL & ~TaintKind.PATH),
|
|
23
|
+
Source(_sym("os.environ"), "environment"),
|
|
24
|
+
Source(_sym("subprocess.run.stdout"), "process-output"),
|
|
25
|
+
Source(_sym("subprocess.check_output"), "process-output"),
|
|
26
|
+
Source(_sym("subprocess.getoutput"), "process-output"),
|
|
27
|
+
Source(_sym("subprocess.Popen.communicate"), "process-output"),
|
|
28
|
+
Sink(_sym("os.system"), TaintKind.COMMAND),
|
|
29
|
+
Sink(_sym("os.popen"), TaintKind.COMMAND),
|
|
30
|
+
Sink(_sym("subprocess.run"), TaintKind.COMMAND),
|
|
31
|
+
Sink(_sym("subprocess.call"), TaintKind.COMMAND),
|
|
32
|
+
Sink(_sym("subprocess.check_call"), TaintKind.COMMAND),
|
|
33
|
+
Sink(_sym("subprocess.check_output"), TaintKind.COMMAND),
|
|
34
|
+
Sink(_sym("subprocess.Popen"), TaintKind.COMMAND),
|
|
35
|
+
Sink(_sym("builtins.eval"), TaintKind.CODE),
|
|
36
|
+
Sink(_sym("builtins.exec"), TaintKind.CODE),
|
|
37
|
+
Sink(_sym("builtins.open"), TaintKind.PATH),
|
|
38
|
+
Sink(_sym("os.remove"), TaintKind.PATH),
|
|
39
|
+
Sink(_sym("os.unlink"), TaintKind.PATH),
|
|
40
|
+
Sink(_sym("os.rmdir"), TaintKind.PATH),
|
|
41
|
+
Sink(_sym("shutil.rmtree"), TaintKind.PATH),
|
|
42
|
+
Sink(_sym("urllib.request.urlopen"), TaintKind.SSRF),
|
|
43
|
+
*(
|
|
44
|
+
Sink(
|
|
45
|
+
_sym(f"sqlite3.connect{cursor}.{method}"),
|
|
46
|
+
TaintKind.SQL | TaintKind.CREDENTIAL,
|
|
47
|
+
((TaintKind.SQL, (0,)),),
|
|
48
|
+
)
|
|
49
|
+
for cursor in ("", ".cursor")
|
|
50
|
+
for method in ("execute", "executemany", "executescript")
|
|
51
|
+
),
|
|
52
|
+
Sanitizer(_sym("shlex.quote"), TaintKind.COMMAND),
|
|
53
|
+
Sanitizer(_sym("html.escape"), TaintKind.HTML),
|
|
54
|
+
Sink(_sym("pickle.loads"), TaintKind.DESERIALIZATION),
|
|
55
|
+
Sink(_sym("pickle.load"), TaintKind.DESERIALIZATION),
|
|
56
|
+
Sink(_sym("pickle.Unpickler"), TaintKind.DESERIALIZATION),
|
|
57
|
+
Sink(_sym("marshal.loads"), TaintKind.DESERIALIZATION),
|
|
58
|
+
Sink(_sym("marshal.load"), TaintKind.DESERIALIZATION),
|
|
59
|
+
Sink(_sym("shelve.open"), TaintKind.DESERIALIZATION),
|
|
60
|
+
Sink(_sym("dill.loads"), TaintKind.DESERIALIZATION),
|
|
61
|
+
Sink(_sym("jsonpickle.decode"), TaintKind.DESERIALIZATION),
|
|
62
|
+
Sink(_sym("yaml.load"), TaintKind.DESERIALIZATION),
|
|
63
|
+
Sink(_sym("yaml.unsafe_load"), TaintKind.DESERIALIZATION),
|
|
64
|
+
Sink(_sym("yaml.full_load"), TaintKind.DESERIALIZATION),
|
|
65
|
+
Sanitizer(_sym("os.path.basename"), TaintKind.PATH),
|
|
66
|
+
Validator(_sym("re.fullmatch"), argument=1),
|
|
67
|
+
Validator(_sym("re.compile.fullmatch")),
|
|
68
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""SQLAlchemy security models: raw SQL execution sinks.
|
|
2
|
+
|
|
3
|
+
Symbols follow the engine's call-chain rule: the result of calling a symbol denotes
|
|
4
|
+
that symbol, so ``create_engine(url).connect().execute`` is
|
|
5
|
+
``python.sqlalchemy.create_engine.connect.execute``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import ClassVar
|
|
11
|
+
|
|
12
|
+
from coretrace_python.plugins import ModelPlugin
|
|
13
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
14
|
+
from coretrace_python.taint import Model, Sanitizer, Sink, TaintKind
|
|
15
|
+
|
|
16
|
+
_EXECUTORS = (
|
|
17
|
+
"sqlalchemy.create_engine",
|
|
18
|
+
"sqlalchemy.create_engine.connect",
|
|
19
|
+
"sqlalchemy.create_engine.begin",
|
|
20
|
+
"sqlalchemy.engine.create_engine",
|
|
21
|
+
"sqlalchemy.engine.create_engine.connect",
|
|
22
|
+
"sqlalchemy.orm.Session",
|
|
23
|
+
"sqlalchemy.orm.sessionmaker",
|
|
24
|
+
"sqlalchemy.orm.scoped_session",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_TEXT_MODULES = ("sqlalchemy", "sqlalchemy.sql", "sqlalchemy.sql.expression")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _sym(path: str) -> SymbolId:
|
|
32
|
+
return SymbolId(f"python.{path}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SqlAlchemyModels(ModelPlugin):
|
|
36
|
+
name: ClassVar[str] = "sqlalchemy-models"
|
|
37
|
+
models: ClassVar[tuple[Model, ...]] = (
|
|
38
|
+
*(
|
|
39
|
+
Sink(_sym(f"{executor}.{method}"), TaintKind.SQL | TaintKind.CREDENTIAL, ((TaintKind.SQL, (0,)),))
|
|
40
|
+
for executor in _EXECUTORS
|
|
41
|
+
for method in ("execute", "exec_driver_sql", "scalar", "scalars")
|
|
42
|
+
),
|
|
43
|
+
# ``text()`` is where SQL text is committed: a sink, and its result carries no
|
|
44
|
+
# further SQL taint so the ``execute`` that follows does not report it twice.
|
|
45
|
+
*(Sink(_sym(f"{module}.text"), TaintKind.SQL) for module in _TEXT_MODULES),
|
|
46
|
+
*(Sanitizer(_sym(f"{module}.text"), TaintKind.SQL) for module in _TEXT_MODULES),
|
|
47
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Hardcoded secrets in configuration files: ``.env``, YAML, TOML, JSON, INI, properties.
|
|
2
|
+
|
|
3
|
+
Judged with the same rules as the Python literal detector, once per project.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from typing import ClassVar
|
|
10
|
+
|
|
11
|
+
from coretrace_python.findings import Finding
|
|
12
|
+
from coretrace_python.plugins import PluginContext, ProjectContext, ProjectPlugin
|
|
13
|
+
from coretrace_python.plugins.secrets import (
|
|
14
|
+
DEFAULT_CREDENTIAL_NAMES,
|
|
15
|
+
DEFAULT_PATTERNS,
|
|
16
|
+
SecretDetector,
|
|
17
|
+
SecretPattern,
|
|
18
|
+
config_literals,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigSecrets(SecretDetector, ProjectPlugin):
|
|
23
|
+
name: ClassVar[str] = "config-secrets"
|
|
24
|
+
patterns: ClassVar[tuple[SecretPattern, ...]] = DEFAULT_PATTERNS
|
|
25
|
+
credential_names: ClassVar[tuple[str, ...]] = DEFAULT_CREDENTIAL_NAMES
|
|
26
|
+
|
|
27
|
+
def analyze(self, ctx: PluginContext) -> Sequence[Finding]:
|
|
28
|
+
return ()
|
|
29
|
+
|
|
30
|
+
def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
|
|
31
|
+
if ctx.root is None:
|
|
32
|
+
return ()
|
|
33
|
+
findings: list[Finding] = []
|
|
34
|
+
for value, name, span, function in config_literals(ctx.root):
|
|
35
|
+
finding = self.judge(value, name, span, function)
|
|
36
|
+
if finding is not None:
|
|
37
|
+
findings.append(finding)
|
|
38
|
+
return findings
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Hardcoded secrets in Python literals: provider formats, credential-like names,
|
|
2
|
+
high-entropy tokens."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
from coretrace_python.plugins.secrets import (
|
|
9
|
+
DEFAULT_CREDENTIAL_NAMES,
|
|
10
|
+
DEFAULT_PATTERNS,
|
|
11
|
+
SecretDetector,
|
|
12
|
+
SecretPattern,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HardcodedSecrets(SecretDetector):
|
|
17
|
+
name: ClassVar[str] = "hardcoded-secrets"
|
|
18
|
+
patterns: ClassVar[tuple[SecretPattern, ...]] = DEFAULT_PATTERNS
|
|
19
|
+
credential_names: ClassVar[tuple[str, ...]] = DEFAULT_CREDENTIAL_NAMES
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Command injection: attacker-controlled input reaching a COMMAND sink."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from coretrace_python.findings import Severity
|
|
8
|
+
from coretrace_python.plugins import TaintDetector
|
|
9
|
+
from coretrace_python.taint import TaintKind
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CommandInjectionPlugin(TaintDetector):
|
|
13
|
+
name: ClassVar[str] = "command-injection"
|
|
14
|
+
rule_id: ClassVar[str] = "command-injection"
|
|
15
|
+
kind: ClassVar[TaintKind] = TaintKind.COMMAND
|
|
16
|
+
severity: ClassVar[Severity] = Severity.HIGH
|
|
17
|
+
title: ClassVar[str] = "Command injection"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Insecure deserialization: attacker-controlled input reaching a insecure deserialization sink."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from coretrace_python.findings import Severity
|
|
8
|
+
from coretrace_python.plugins import TaintDetector
|
|
9
|
+
from coretrace_python.taint import TaintKind
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InsecureDeserializationPlugin(TaintDetector):
|
|
13
|
+
name: ClassVar[str] = "insecure-deserialization"
|
|
14
|
+
rule_id: ClassVar[str] = "insecure-deserialization"
|
|
15
|
+
kind: ClassVar[TaintKind] = TaintKind.DESERIALIZATION
|
|
16
|
+
severity: ClassVar[Severity] = Severity.CRITICAL
|
|
17
|
+
title: ClassVar[str] = "Insecure deserialization"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
name = "insecure-deserialization"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
plugin_api = ">=1,<2"
|
|
4
|
+
requires = ["taint.flows", "findings.refutation"]
|
|
5
|
+
provides = ["vulnerability.insecure-deserialization"]
|
|
6
|
+
|
|
7
|
+
[entrypoint]
|
|
8
|
+
module = "insecure_deserialization"
|
|
9
|
+
class = "InsecureDeserializationPlugin"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Open redirect: attacker-controlled input reaching a open redirect sink."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from coretrace_python.findings import Severity
|
|
8
|
+
from coretrace_python.plugins import TaintDetector
|
|
9
|
+
from coretrace_python.taint import TaintKind
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OpenRedirectPlugin(TaintDetector):
|
|
13
|
+
name: ClassVar[str] = "open-redirect"
|
|
14
|
+
rule_id: ClassVar[str] = "open-redirect"
|
|
15
|
+
kind: ClassVar[TaintKind] = TaintKind.REDIRECT
|
|
16
|
+
severity: ClassVar[Severity] = Severity.MEDIUM
|
|
17
|
+
title: ClassVar[str] = "Open redirect"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Path traversal: attacker-controlled input reaching a PATH sink."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from coretrace_python.findings import Severity
|
|
8
|
+
from coretrace_python.plugins import TaintDetector
|
|
9
|
+
from coretrace_python.taint import TaintKind
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PathTraversalPlugin(TaintDetector):
|
|
13
|
+
name: ClassVar[str] = "path-traversal"
|
|
14
|
+
rule_id: ClassVar[str] = "path-traversal"
|
|
15
|
+
kind: ClassVar[TaintKind] = TaintKind.PATH
|
|
16
|
+
severity: ClassVar[Severity] = Severity.HIGH
|
|
17
|
+
title: ClassVar[str] = "Path traversal"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Plaintext credentials in database queries: a password-named parameter reaching a
|
|
2
|
+
database statement without passing through a hashing function, whether the statement
|
|
3
|
+
stores it or compares it, since comparing in clear implies storing in clear. A name is
|
|
4
|
+
a hint, so medium confidence."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import ClassVar
|
|
9
|
+
|
|
10
|
+
from coretrace_python.findings import Confidence, Severity
|
|
11
|
+
from coretrace_python.plugins import TaintDetector
|
|
12
|
+
from coretrace_python.taint import TaintKind
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PlaintextCredentialsPlugin(TaintDetector):
|
|
16
|
+
name: ClassVar[str] = "plaintext-credentials"
|
|
17
|
+
rule_id: ClassVar[str] = "plaintext-credential-storage"
|
|
18
|
+
kind: ClassVar[TaintKind] = TaintKind.CREDENTIAL
|
|
19
|
+
severity: ClassVar[Severity] = Severity.HIGH
|
|
20
|
+
confidence: ClassVar[Confidence] = Confidence.MEDIUM
|
|
21
|
+
title: ClassVar[str] = "Plaintext credential in a database query"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
name = "plaintext-credentials"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
plugin_api = ">=1,<2"
|
|
4
|
+
requires = ["taint.flows", "findings.refutation"]
|
|
5
|
+
provides = ["vulnerability.plaintext-credential-storage"]
|
|
6
|
+
|
|
7
|
+
[entrypoint]
|
|
8
|
+
module = "plaintext_credentials"
|
|
9
|
+
class = "PlaintextCredentialsPlugin"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""SQL injection: attacker-controlled input reaching a SQL sink."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from coretrace_python.findings import Severity
|
|
8
|
+
from coretrace_python.plugins import TaintDetector
|
|
9
|
+
from coretrace_python.taint import TaintKind
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SqlInjectionPlugin(TaintDetector):
|
|
13
|
+
name: ClassVar[str] = "sql-injection"
|
|
14
|
+
rule_id: ClassVar[str] = "sql-injection"
|
|
15
|
+
kind: ClassVar[TaintKind] = TaintKind.SQL
|
|
16
|
+
severity: ClassVar[Severity] = Severity.HIGH
|
|
17
|
+
title: ClassVar[str] = "SQL injection"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Server-side request forgery: attacker-controlled input reaching a SSRF sink."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from coretrace_python.findings import Severity
|
|
8
|
+
from coretrace_python.plugins import TaintDetector
|
|
9
|
+
from coretrace_python.taint import TaintKind
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SsrfPlugin(TaintDetector):
|
|
13
|
+
name: ClassVar[str] = "ssrf"
|
|
14
|
+
rule_id: ClassVar[str] = "ssrf"
|
|
15
|
+
kind: ClassVar[TaintKind] = TaintKind.SSRF
|
|
16
|
+
severity: ClassVar[Severity] = Severity.HIGH
|
|
17
|
+
title: ClassVar[str] = "Server-side request forgery"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Cross-site scripting: attacker-controlled input reaching a HTML sink."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from coretrace_python.findings import Severity
|
|
8
|
+
from coretrace_python.plugins import TaintDetector
|
|
9
|
+
from coretrace_python.taint import TaintKind
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class XssPlugin(TaintDetector):
|
|
13
|
+
name: ClassVar[str] = "xss"
|
|
14
|
+
rule_id: ClassVar[str] = "xss"
|
|
15
|
+
kind: ClassVar[TaintKind] = TaintKind.HTML
|
|
16
|
+
severity: ClassVar[Severity] = Severity.HIGH
|
|
17
|
+
title: ClassVar[str] = "Cross-site scripting"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Report calls to the dynamic-code builtins, whatever name the file gives them."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from coretrace_python.findings import Severity
|
|
8
|
+
from coretrace_python.plugins import SymbolCallDetector
|
|
9
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DangerousEvalPlugin(SymbolCallDetector):
|
|
13
|
+
name: ClassVar[str] = "dangerous-eval"
|
|
14
|
+
rule_id: ClassVar[str] = "dangerous-eval"
|
|
15
|
+
symbols: ClassVar[frozenset[SymbolId]] = frozenset(
|
|
16
|
+
{SymbolId("python.builtins.eval"), SymbolId("python.builtins.exec")}
|
|
17
|
+
)
|
|
18
|
+
severity: ClassVar[Severity] = Severity.HIGH
|
|
19
|
+
message_template: ClassVar[str] = "call to {symbol} executes dynamically built code"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""``app.run(debug=True)``: the Werkzeug debugger executes code on any exception."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Sequence
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
from coretrace_python.analysis import AnyAnalysis
|
|
9
|
+
from coretrace_python.findings import Confidence, Finding, Severity
|
|
10
|
+
from coretrace_python.hir import nodes
|
|
11
|
+
from coretrace_python.hir.visitors import Node, children
|
|
12
|
+
from coretrace_python.plugins import Plugin, PluginContext
|
|
13
|
+
from coretrace_python.semantic.scopes import ScopeAnalysis, ScopeId, ScopeTable
|
|
14
|
+
from coretrace_python.semantic.symbols import SymbolAnalysis, SymbolId
|
|
15
|
+
|
|
16
|
+
RUN = SymbolId("python.flask.Flask.run")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FlaskDebugPlugin(Plugin):
|
|
20
|
+
name: ClassVar[str] = "flask-debug"
|
|
21
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({ScopeAnalysis, SymbolAnalysis})
|
|
22
|
+
|
|
23
|
+
def analyze(self, ctx: PluginContext) -> Sequence[Finding]:
|
|
24
|
+
scopes = ctx.get(ScopeAnalysis)
|
|
25
|
+
symbols = ctx.get(SymbolAnalysis)
|
|
26
|
+
findings: list[Finding] = []
|
|
27
|
+
for call, scope, function in _calls(ctx.module.body, scopes, scopes.module_scope.id, None):
|
|
28
|
+
if symbols.resolve_expression(scope, call.callee) != RUN:
|
|
29
|
+
continue
|
|
30
|
+
if not any(
|
|
31
|
+
k.name == "debug" and isinstance(k.value, nodes.Constant) and k.value.value is True
|
|
32
|
+
for k in call.keywords
|
|
33
|
+
):
|
|
34
|
+
continue
|
|
35
|
+
findings.append(
|
|
36
|
+
Finding(
|
|
37
|
+
"debug-enabled",
|
|
38
|
+
"Flask debugger enabled: app.run(debug=True) exposes an interactive "
|
|
39
|
+
"debugger able to execute Python on any exception",
|
|
40
|
+
Severity.HIGH,
|
|
41
|
+
Confidence.HIGH,
|
|
42
|
+
call.span,
|
|
43
|
+
function,
|
|
44
|
+
{"symbol": str(RUN)},
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return findings
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _calls(
|
|
51
|
+
body: Sequence[Node], scopes: ScopeTable, scope: ScopeId, function: str | None
|
|
52
|
+
) -> Iterator[tuple[nodes.Call, ScopeId, str | None]]:
|
|
53
|
+
for node in body:
|
|
54
|
+
if isinstance(node, nodes.Function):
|
|
55
|
+
yield from _calls(node.body, scopes, scopes.scope_for(node).id, node.name)
|
|
56
|
+
elif isinstance(node, nodes.Class):
|
|
57
|
+
yield from _calls(node.body, scopes, scopes.scope_for(node).id, None)
|
|
58
|
+
elif isinstance(node, nodes.Lambda | nodes.Comprehension):
|
|
59
|
+
continue
|
|
60
|
+
else:
|
|
61
|
+
if isinstance(node, nodes.Call):
|
|
62
|
+
yield node, scope, function
|
|
63
|
+
yield from _calls(list(children(node)), scopes, scope, function)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""HTTP client calls without a ``timeout``: a slow or silent peer blocks the caller."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
from coretrace_python.analysis import AnyAnalysis
|
|
9
|
+
from coretrace_python.findings import Confidence, Finding, Severity
|
|
10
|
+
from coretrace_python.interprocedural import CallGraphAnalysis
|
|
11
|
+
from coretrace_python.ir.model import Call
|
|
12
|
+
from coretrace_python.ir.ssa import SSAAnalysis
|
|
13
|
+
from coretrace_python.plugins import Plugin, PluginContext
|
|
14
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
15
|
+
|
|
16
|
+
_METHODS = ("get", "post", "put", "patch", "delete", "head", "options", "request")
|
|
17
|
+
_CALLERS = ("requests", "requests.Session", "requests.api", "httpx", "httpx.Client", "httpx.AsyncClient")
|
|
18
|
+
HTTP_CALLS = frozenset(SymbolId(f"python.{caller}.{method}") for caller in _CALLERS for method in _METHODS)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MissingTimeoutPlugin(Plugin):
|
|
22
|
+
name: ClassVar[str] = "missing-timeout"
|
|
23
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis, CallGraphAnalysis})
|
|
24
|
+
|
|
25
|
+
def analyze(self, ctx: PluginContext) -> Sequence[Finding]:
|
|
26
|
+
graph = ctx.get(CallGraphAnalysis)
|
|
27
|
+
findings: list[Finding] = []
|
|
28
|
+
for function in ctx.functions():
|
|
29
|
+
symbols = graph.symbols(graph.name_of(function))
|
|
30
|
+
for block in ctx.get(SSAAnalysis, function).blocks:
|
|
31
|
+
for instruction in block.instructions:
|
|
32
|
+
if not isinstance(instruction, Call):
|
|
33
|
+
continue
|
|
34
|
+
symbol = symbols.get(instruction.callee)
|
|
35
|
+
if symbol is None or symbol not in HTTP_CALLS:
|
|
36
|
+
continue
|
|
37
|
+
if any(name == "timeout" for name, _ in instruction.keywords):
|
|
38
|
+
continue
|
|
39
|
+
findings.append(
|
|
40
|
+
Finding(
|
|
41
|
+
"missing-timeout",
|
|
42
|
+
f"{symbol} is called without a timeout; a slow or silent peer "
|
|
43
|
+
"blocks this code indefinitely",
|
|
44
|
+
Severity.LOW,
|
|
45
|
+
Confidence.HIGH,
|
|
46
|
+
instruction.location,
|
|
47
|
+
function.name,
|
|
48
|
+
{"symbol": str(symbol)},
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
return findings
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Report uses of broken hash algorithms."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from coretrace_python.findings import Severity
|
|
8
|
+
from coretrace_python.plugins import SymbolCallDetector
|
|
9
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WeakCryptoPlugin(SymbolCallDetector):
|
|
13
|
+
name: ClassVar[str] = "weak-crypto"
|
|
14
|
+
rule_id: ClassVar[str] = "weak-crypto"
|
|
15
|
+
symbols: ClassVar[frozenset[SymbolId]] = frozenset(
|
|
16
|
+
{SymbolId("python.hashlib.md5"), SymbolId("python.hashlib.sha1")}
|
|
17
|
+
)
|
|
18
|
+
severity: ClassVar[Severity] = Severity.MEDIUM
|
|
19
|
+
message_template: ClassVar[str] = "call to {symbol} uses a broken hash algorithm"
|