flawed 0.0.1__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.
- flawed/__init__.py +43 -0
- flawed/calls.py +202 -0
- flawed/checks.py +166 -0
- flawed/collections.py +317 -0
- flawed/conditions.py +120 -0
- flawed/core.py +127 -0
- flawed/detector.py +52 -0
- flawed/effects.py +258 -0
- flawed/evidence.py +125 -0
- flawed/flow.py +148 -0
- flawed/function.py +224 -0
- flawed/inputs.py +305 -0
- flawed/repo.py +92 -0
- flawed/route.py +203 -0
- flawed/scopes.py +204 -0
- flawed-0.0.1.dist-info/METADATA +4 -0
- flawed-0.0.1.dist-info/RECORD +18 -0
- flawed-0.0.1.dist-info/WHEEL +4 -0
flawed/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Rift: static analysis engine for confusion vulnerability research.
|
|
2
|
+
|
|
3
|
+
Rule authors start here::
|
|
4
|
+
|
|
5
|
+
from flawed import open_repo, detector
|
|
6
|
+
from flawed.inputs import Query, Form, Json
|
|
7
|
+
from flawed.effects import Mutation, Session
|
|
8
|
+
from flawed.checks import Crypto, Token
|
|
9
|
+
from flawed.route import Route, POST, accepting
|
|
10
|
+
|
|
11
|
+
The top-level package IS the Rule API. Deeper layers live in subpackages:
|
|
12
|
+
|
|
13
|
+
- ``flawed.semantic`` — Semantic Layer (framework interpretation)
|
|
14
|
+
- ``flawed.index`` — Code Index (structural extraction)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from flawed.detector import detector
|
|
20
|
+
from flawed.repo import RepoView
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def open_repo(path: str) -> RepoView:
|
|
24
|
+
"""Load an analyzed repository and return the top-level navigation object.
|
|
25
|
+
|
|
26
|
+
Constructs a Code Index (Layer 1), runs the Semantic Layer
|
|
27
|
+
interpreters (Layer 2), and returns a :class:`RepoView` -- the
|
|
28
|
+
single entry point for all navigation and detection.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
path: Path to the analysis store snapshot directory.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A :class:`RepoView` for querying the analyzed repository.
|
|
35
|
+
"""
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"RepoView",
|
|
41
|
+
"detector",
|
|
42
|
+
"open_repo",
|
|
43
|
+
]
|
flawed/calls.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Call site, argument, and function selector types.
|
|
2
|
+
|
|
3
|
+
A :class:`CallSite` represents a specific location where a function is
|
|
4
|
+
called with specific arguments. This is distinct from
|
|
5
|
+
:class:`~flawed.function.Function` -- a ``Function`` is the
|
|
6
|
+
*definition*, while a ``CallSite`` is a particular *invocation* with
|
|
7
|
+
its actual arguments and return value handle.
|
|
8
|
+
|
|
9
|
+
The :class:`FnSelector` type and its :class:`Fn` sugar namespace
|
|
10
|
+
provide composable selectors for matching functions by name, FQN, or
|
|
11
|
+
pattern. Selectors compose with ``|``::
|
|
12
|
+
|
|
13
|
+
from flawed.calls import Fn
|
|
14
|
+
|
|
15
|
+
selector = Fn.named("execute") | Fn.fqn("sqlalchemy.Session.add")
|
|
16
|
+
calls = route.reachable.calls(selector)
|
|
17
|
+
|
|
18
|
+
for call in calls:
|
|
19
|
+
print(call.target, call.arguments)
|
|
20
|
+
if call.return_value.flows_to(some_effect.target):
|
|
21
|
+
print("Return value reaches the effect")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from flawed.core import Location
|
|
31
|
+
from flawed.flow import ValueHandle
|
|
32
|
+
from flawed.function import Function
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class Argument:
|
|
37
|
+
"""A specific argument at a call site.
|
|
38
|
+
|
|
39
|
+
Represents one positional or keyword argument passed at a
|
|
40
|
+
particular call site. The :attr:`value` property provides a
|
|
41
|
+
:class:`~flawed.flow.ValueHandle` for tracking the argument
|
|
42
|
+
value through the program.
|
|
43
|
+
|
|
44
|
+
Example::
|
|
45
|
+
|
|
46
|
+
call = route.reachable.calls(Fn.named("execute")).first()
|
|
47
|
+
for arg in call.arguments:
|
|
48
|
+
print(arg.index, arg.name, arg.expression)
|
|
49
|
+
if arg.value.derived_from(Json()):
|
|
50
|
+
print("Argument comes from JSON input!")
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
index: int
|
|
54
|
+
"""0-based positional index of the argument."""
|
|
55
|
+
|
|
56
|
+
name: str | None
|
|
57
|
+
"""Keyword name if passed as a keyword argument, or ``None``."""
|
|
58
|
+
|
|
59
|
+
expression: str
|
|
60
|
+
"""Source text of the argument expression."""
|
|
61
|
+
|
|
62
|
+
location: Location
|
|
63
|
+
"""Source location of the argument expression."""
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def value(self) -> ValueHandle:
|
|
67
|
+
"""Handle for tracking this argument's value through the program."""
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class CallSite:
|
|
73
|
+
"""A specific invocation of a function with specific arguments.
|
|
74
|
+
|
|
75
|
+
Distinct from :class:`~flawed.function.Function` -- a function
|
|
76
|
+
may be called from many sites, and each site carries its own
|
|
77
|
+
arguments and return value.
|
|
78
|
+
|
|
79
|
+
The :attr:`target` is ``None`` when the called function cannot be
|
|
80
|
+
resolved (e.g. a call through a variable whose value is unknown).
|
|
81
|
+
|
|
82
|
+
Example::
|
|
83
|
+
|
|
84
|
+
for call in route.reachable.calls(Fn.named("execute")):
|
|
85
|
+
print(call.target_expression)
|
|
86
|
+
arg0 = call.argument(0)
|
|
87
|
+
if arg0.value.derived_from(Json()):
|
|
88
|
+
yield route.finding("SQL from JSON").evidence(call, "db call")
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
target: Function | None
|
|
92
|
+
"""The called function, or ``None`` if the target is unresolved."""
|
|
93
|
+
|
|
94
|
+
target_expression: str
|
|
95
|
+
"""Source text of the call target (e.g. ``"db.execute"``)."""
|
|
96
|
+
|
|
97
|
+
arguments: tuple[Argument, ...]
|
|
98
|
+
"""All arguments in call-site order."""
|
|
99
|
+
|
|
100
|
+
location: Location
|
|
101
|
+
"""Source location of the call expression."""
|
|
102
|
+
|
|
103
|
+
function: Function
|
|
104
|
+
"""The function containing this call site."""
|
|
105
|
+
|
|
106
|
+
def argument(self, index: int) -> Argument:
|
|
107
|
+
"""Look up an argument by 0-based positional index.
|
|
108
|
+
|
|
109
|
+
Raises ``IndexError`` if the index is out of range.
|
|
110
|
+
"""
|
|
111
|
+
raise NotImplementedError
|
|
112
|
+
|
|
113
|
+
def keyword_argument(self, name: str) -> Argument | None:
|
|
114
|
+
"""Look up an argument by keyword name.
|
|
115
|
+
|
|
116
|
+
Returns ``None`` if no keyword argument with the given name
|
|
117
|
+
exists at this call site.
|
|
118
|
+
"""
|
|
119
|
+
raise NotImplementedError
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def return_value(self) -> ValueHandle:
|
|
123
|
+
"""Handle for tracking the call's return value.
|
|
124
|
+
|
|
125
|
+
Use to check whether the return value flows to a particular
|
|
126
|
+
effect or is used in a condition.
|
|
127
|
+
"""
|
|
128
|
+
raise NotImplementedError
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True)
|
|
132
|
+
class FnSelector:
|
|
133
|
+
"""Composable selector for filtering functions by name, FQN, or pattern.
|
|
134
|
+
|
|
135
|
+
Single selectors match by one criterion (name, FQN, or regex).
|
|
136
|
+
Composed selectors (via ``|``) match if *any* alternative matches.
|
|
137
|
+
|
|
138
|
+
Construct via the :class:`Fn` sugar namespace rather than directly::
|
|
139
|
+
|
|
140
|
+
selector = Fn.named("execute") | Fn.fqn("db.Session.add")
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
name_filter: str | None = None
|
|
144
|
+
"""Match functions with this exact short name."""
|
|
145
|
+
|
|
146
|
+
fqn_filter: str | None = None
|
|
147
|
+
"""Match functions with this exact fully qualified name."""
|
|
148
|
+
|
|
149
|
+
pattern_filter: str | None = None
|
|
150
|
+
"""Match functions whose name matches this regex pattern."""
|
|
151
|
+
|
|
152
|
+
_alternatives: tuple[FnSelector, ...] = ()
|
|
153
|
+
"""Internal: composed alternatives from ``|`` operations."""
|
|
154
|
+
|
|
155
|
+
def __or__(self, other: FnSelector) -> FnSelector:
|
|
156
|
+
"""Compose selectors: the result matches if either matches.
|
|
157
|
+
|
|
158
|
+
Example::
|
|
159
|
+
|
|
160
|
+
combined = Fn.named("execute") | Fn.named("run_query")
|
|
161
|
+
"""
|
|
162
|
+
my = self._alternatives if self._alternatives else (self,)
|
|
163
|
+
theirs = other._alternatives if other._alternatives else (other,)
|
|
164
|
+
return FnSelector(_alternatives=(*my, *theirs))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Fn:
|
|
168
|
+
"""Sugar namespace for constructing function selectors.
|
|
169
|
+
|
|
170
|
+
Example::
|
|
171
|
+
|
|
172
|
+
Fn.named("execute") # match by short name
|
|
173
|
+
Fn.fqn("sqlalchemy.Session.add") # match by FQN
|
|
174
|
+
Fn.matching(r"^(get|fetch)_") # match by regex
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def named(name: str) -> FnSelector:
|
|
179
|
+
"""Select functions with the given short name.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
name: Exact function name to match (e.g. ``"execute"``).
|
|
183
|
+
"""
|
|
184
|
+
return FnSelector(name_filter=name)
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def fqn(fqn: str) -> FnSelector:
|
|
188
|
+
"""Select functions with the given fully qualified name.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
fqn: Exact FQN to match (e.g. ``"hmac.compare_digest"``).
|
|
192
|
+
"""
|
|
193
|
+
return FnSelector(fqn_filter=fqn)
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def matching(pattern: str) -> FnSelector:
|
|
197
|
+
"""Select functions matching the given regex pattern.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
pattern: Regular expression matched against the function name.
|
|
201
|
+
"""
|
|
202
|
+
return FnSelector(pattern_filter=pattern)
|
flawed/checks.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Composable selectors for security-relevant validation functions.
|
|
2
|
+
|
|
3
|
+
Parallel to the effects module: where effects describe *what side
|
|
4
|
+
effects occur*, checks describe *what validation functions are called*.
|
|
5
|
+
Use checks to determine whether a route validates its inputs before
|
|
6
|
+
performing a sensitive operation.
|
|
7
|
+
|
|
8
|
+
The selector namespaces are:
|
|
9
|
+
|
|
10
|
+
- :class:`Crypto` -- cryptographic comparison and hashing
|
|
11
|
+
- :class:`Token` -- JWT / signed-token verification
|
|
12
|
+
- :class:`Schema` -- input schema validation (pydantic, marshmallow, etc.)
|
|
13
|
+
- :class:`Permission` -- authorization / permission checks (heuristic)
|
|
14
|
+
|
|
15
|
+
Compose checks with effects to express security patterns::
|
|
16
|
+
|
|
17
|
+
from flawed.checks import Crypto, Token, Schema, Permission
|
|
18
|
+
from flawed.calls import Fn
|
|
19
|
+
from flawed.effects import Mutation, Session
|
|
20
|
+
|
|
21
|
+
VALIDATORS = Crypto.compare() | Token.verify()
|
|
22
|
+
SENSITIVE = Mutation.any() | Session.write()
|
|
23
|
+
|
|
24
|
+
validators = route.reachable.calls(VALIDATORS).with_argument_from(read.value)
|
|
25
|
+
|
|
26
|
+
Permission selectors use name-pattern matching (heuristic) since
|
|
27
|
+
authorization functions are project-specific. Extend with
|
|
28
|
+
``Fn.fqn()`` for precision when the target project's auth functions
|
|
29
|
+
are known.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from flawed.calls import Fn, FnSelector
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Crypto:
|
|
38
|
+
"""Selectors for cryptographic verification functions.
|
|
39
|
+
|
|
40
|
+
Example::
|
|
41
|
+
|
|
42
|
+
Crypto.compare() # hmac.compare_digest, check_password_hash, etc.
|
|
43
|
+
Crypto.hash() # hashlib.sha256, hashlib.pbkdf2_hmac, etc.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def compare() -> FnSelector:
|
|
48
|
+
"""Hash/digest comparison functions.
|
|
49
|
+
|
|
50
|
+
Matches: ``hmac.compare_digest``, ``werkzeug.security.check_password_hash``,
|
|
51
|
+
``bcrypt.checkpw``, ``passlib.hash.bcrypt.verify``, ``argon2.verify_password``.
|
|
52
|
+
"""
|
|
53
|
+
return (
|
|
54
|
+
Fn.fqn("hmac.compare_digest")
|
|
55
|
+
| Fn.fqn("werkzeug.security.check_password_hash")
|
|
56
|
+
| Fn.fqn("bcrypt.checkpw")
|
|
57
|
+
| Fn.fqn("passlib.hash.bcrypt.verify")
|
|
58
|
+
| Fn.fqn("argon2.verify_password")
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def hash() -> FnSelector:
|
|
63
|
+
"""Cryptographic hash construction functions.
|
|
64
|
+
|
|
65
|
+
Matches: ``hashlib.sha256``, ``hashlib.sha512``, ``hashlib.sha384``,
|
|
66
|
+
``hashlib.sha3_256``, ``hashlib.pbkdf2_hmac``, ``hashlib.scrypt``.
|
|
67
|
+
"""
|
|
68
|
+
return (
|
|
69
|
+
Fn.fqn("hashlib.sha256")
|
|
70
|
+
| Fn.fqn("hashlib.sha512")
|
|
71
|
+
| Fn.fqn("hashlib.sha384")
|
|
72
|
+
| Fn.fqn("hashlib.sha3_256")
|
|
73
|
+
| Fn.fqn("hashlib.pbkdf2_hmac")
|
|
74
|
+
| Fn.fqn("hashlib.scrypt")
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Token:
|
|
79
|
+
"""Selectors for token verification functions.
|
|
80
|
+
|
|
81
|
+
Example::
|
|
82
|
+
|
|
83
|
+
Token.verify() # jwt.decode, itsdangerous loads, etc.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def verify() -> FnSelector:
|
|
88
|
+
"""Token decode / verification functions.
|
|
89
|
+
|
|
90
|
+
Matches: ``jwt.decode``, ``jose.jwt.decode``, ``authlib.jose.jwt.decode``,
|
|
91
|
+
``itsdangerous.URLSafeTimedSerializer.loads``,
|
|
92
|
+
``itsdangerous.URLSafeSerializer.loads``, ``itsdangerous.Signer.unsign``.
|
|
93
|
+
"""
|
|
94
|
+
return (
|
|
95
|
+
Fn.fqn("jwt.decode")
|
|
96
|
+
| Fn.fqn("jose.jwt.decode")
|
|
97
|
+
| Fn.fqn("authlib.jose.jwt.decode")
|
|
98
|
+
| Fn.fqn("itsdangerous.URLSafeTimedSerializer.loads")
|
|
99
|
+
| Fn.fqn("itsdangerous.URLSafeSerializer.loads")
|
|
100
|
+
| Fn.fqn("itsdangerous.Signer.unsign")
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class Schema:
|
|
105
|
+
"""Selectors for schema / input validation functions.
|
|
106
|
+
|
|
107
|
+
Example::
|
|
108
|
+
|
|
109
|
+
Schema.validate() # pydantic, marshmallow, wtforms, cerberus, etc.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def validate() -> FnSelector:
|
|
114
|
+
"""Schema validation functions.
|
|
115
|
+
|
|
116
|
+
Matches: ``pydantic.BaseModel.model_validate``,
|
|
117
|
+
``marshmallow.Schema.load``, ``wtforms.Form.validate``,
|
|
118
|
+
``cerberus.Validator.validate``, ``voluptuous.Schema.__call__``,
|
|
119
|
+
and their common variants.
|
|
120
|
+
"""
|
|
121
|
+
return (
|
|
122
|
+
Fn.fqn("pydantic.BaseModel.model_validate")
|
|
123
|
+
| Fn.fqn("pydantic.BaseModel.model_validate_json")
|
|
124
|
+
| Fn.fqn("marshmallow.Schema.load")
|
|
125
|
+
| Fn.fqn("marshmallow.Schema.loads")
|
|
126
|
+
| Fn.fqn("wtforms.Form.validate")
|
|
127
|
+
| Fn.fqn("wtforms.Form.validate_on_submit")
|
|
128
|
+
| Fn.fqn("cerberus.Validator.validate")
|
|
129
|
+
| Fn.fqn("voluptuous.Schema.__call__")
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Permission:
|
|
134
|
+
"""Selectors for authorization / permission check functions.
|
|
135
|
+
|
|
136
|
+
These use name matching (heuristic) since permission check
|
|
137
|
+
functions are project-specific. Extend with ``Fn.fqn()`` for
|
|
138
|
+
precision when the target project's auth functions are known.
|
|
139
|
+
|
|
140
|
+
Example::
|
|
141
|
+
|
|
142
|
+
Permission.check() # has_permission, authorize, etc.
|
|
143
|
+
Permission.require() # require_permission, require_role, etc.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def check() -> FnSelector:
|
|
148
|
+
"""Common permission-check function names.
|
|
149
|
+
|
|
150
|
+
Matches: ``has_permission``, ``check_permission``, ``can_access``,
|
|
151
|
+
``authorize``, ``is_authorized``.
|
|
152
|
+
"""
|
|
153
|
+
return Fn.matching(
|
|
154
|
+
r"^(has_permission|check_permission|can_access|authorize|is_authorized)$"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def require() -> FnSelector:
|
|
159
|
+
"""Common permission-require function names.
|
|
160
|
+
|
|
161
|
+
Matches: ``require_permission``, ``require_role``,
|
|
162
|
+
``require_admin``, ``require_auth``.
|
|
163
|
+
"""
|
|
164
|
+
return Fn.matching(
|
|
165
|
+
r"^(require_permission|require_role|require_admin|require_auth)$"
|
|
166
|
+
)
|