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 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
+ )