fluidattacks_core_contextualizes 2.0.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.
@@ -0,0 +1,48 @@
1
+ from fluidattacks_core.contextualizes.components import (
2
+ MANIFEST_KINDS,
3
+ Component,
4
+ ComponentKind,
5
+ Enrichment,
6
+ kind_for_manifests,
7
+ )
8
+ from fluidattacks_core.contextualizes.envelope import (
9
+ SCHEMA_VERSION,
10
+ ContextualizerOutput,
11
+ Repository,
12
+ RunStatus,
13
+ )
14
+ from fluidattacks_core.contextualizes.observations import Observation, ObservationKind
15
+ from fluidattacks_core.contextualizes.paths import (
16
+ MAX_PATH_LENGTH,
17
+ REPOSITORY_ROOT,
18
+ RelativePath,
19
+ ensure_relative_path,
20
+ )
21
+ from fluidattacks_core.contextualizes.schema import (
22
+ SCHEMA_PATH,
23
+ build_schema,
24
+ load_schema,
25
+ render_schema,
26
+ )
27
+
28
+ __all__ = [
29
+ "MANIFEST_KINDS",
30
+ "MAX_PATH_LENGTH",
31
+ "REPOSITORY_ROOT",
32
+ "SCHEMA_PATH",
33
+ "SCHEMA_VERSION",
34
+ "Component",
35
+ "ComponentKind",
36
+ "ContextualizerOutput",
37
+ "Enrichment",
38
+ "Observation",
39
+ "ObservationKind",
40
+ "RelativePath",
41
+ "Repository",
42
+ "RunStatus",
43
+ "build_schema",
44
+ "ensure_relative_path",
45
+ "kind_for_manifests",
46
+ "load_schema",
47
+ "render_schema",
48
+ ]
@@ -0,0 +1,111 @@
1
+ from collections.abc import Iterable
2
+ from enum import StrEnum
3
+ from pathlib import PurePosixPath
4
+ from typing import Annotated, ClassVar
5
+
6
+ from fluidattacks_core.filesystem.defaults import MANIFEST_MARKERS, WORKSPACE_MARKERS
7
+ from fluidattacks_core.filesystem.language import Language
8
+ from pydantic import BaseModel, ConfigDict, Field, StringConstraints
9
+
10
+ from fluidattacks_core.contextualizes.paths import RelativePath
11
+
12
+
13
+ class ComponentKind(StrEnum):
14
+ """Ecosystem of a component, decided by a filename, never by a model.
15
+
16
+ Spelled as the platform's package ecosystem ids so a component joins the
17
+ package inventory without a translation table. `gradle` and `sbt` have no
18
+ id of their own there; both resolve from the maven registry.
19
+ """
20
+
21
+ CARGO = "cargo"
22
+ GO = "go"
23
+ GRADLE = "gradle"
24
+ MAVEN = "maven"
25
+ NPM = "npm"
26
+ NUGET = "nuget"
27
+ PACKAGIST = "packagist"
28
+ PUB = "pub"
29
+ PYPI = "pypi"
30
+ RUBYGEMS = "rubygems"
31
+ SBT = "sbt"
32
+ SWIFTURL = "swifturl"
33
+ UNKNOWN = "unknown"
34
+
35
+
36
+ _GRADLE_FILES = WORKSPACE_MARKERS["gradle"]["declares"] | WORKSPACE_MARKERS["gradle"]["claims"]
37
+
38
+ # Derived, not copied, so an upstream manifest cannot resolve to UNKNOWN here.
39
+ MANIFEST_KINDS: dict[ComponentKind, frozenset[str]] = {
40
+ ComponentKind.CARGO: MANIFEST_MARKERS[Language.Rust],
41
+ ComponentKind.GO: MANIFEST_MARKERS[Language.Go],
42
+ ComponentKind.GRADLE: (
43
+ MANIFEST_MARKERS[Language.Kotlin] | (MANIFEST_MARKERS[Language.Java] & _GRADLE_FILES)
44
+ ),
45
+ ComponentKind.MAVEN: MANIFEST_MARKERS[Language.Java] - _GRADLE_FILES,
46
+ ComponentKind.NPM: (
47
+ MANIFEST_MARKERS[Language.JavaScript] | MANIFEST_MARKERS[Language.TypeScript]
48
+ ),
49
+ ComponentKind.NUGET: MANIFEST_MARKERS[Language.CSharp],
50
+ ComponentKind.PACKAGIST: MANIFEST_MARKERS[Language.PHP],
51
+ ComponentKind.PUB: MANIFEST_MARKERS[Language.Dart],
52
+ ComponentKind.PYPI: MANIFEST_MARKERS[Language.Python],
53
+ ComponentKind.RUBYGEMS: MANIFEST_MARKERS[Language.Ruby],
54
+ ComponentKind.SBT: MANIFEST_MARKERS[Language.Scala],
55
+ ComponentKind.SWIFTURL: MANIFEST_MARKERS[Language.Swift],
56
+ }
57
+
58
+ # A case-insensitive checkout returns whatever spelling it stored.
59
+ _KIND_BY_MANIFEST: dict[str, ComponentKind] = {
60
+ manifest.casefold(): kind
61
+ for kind, manifests in MANIFEST_KINDS.items()
62
+ for manifest in manifests
63
+ }
64
+
65
+ # A polyglot component belongs to the ecosystem that ships it, not the one
66
+ # that tools it: maturin is pypi, gradle wrapping npm is gradle.
67
+ _KIND_PRIORITY: dict[ComponentKind, int] = {
68
+ ComponentKind.GRADLE: 90,
69
+ ComponentKind.MAVEN: 85,
70
+ ComponentKind.SBT: 80,
71
+ ComponentKind.SWIFTURL: 75,
72
+ ComponentKind.PUB: 70,
73
+ ComponentKind.NUGET: 65,
74
+ ComponentKind.GO: 60,
75
+ ComponentKind.PYPI: 55,
76
+ ComponentKind.CARGO: 50,
77
+ ComponentKind.RUBYGEMS: 45,
78
+ ComponentKind.PACKAGIST: 40,
79
+ ComponentKind.NPM: 35,
80
+ }
81
+
82
+
83
+ def kind_for_manifests(manifests: Iterable[str]) -> ComponentKind:
84
+ """Pick a component's kind from the manifest file names found in it."""
85
+ matched = {
86
+ _KIND_BY_MANIFEST[key]
87
+ for name in manifests
88
+ if (key := PurePosixPath(name.replace("\\", "/")).name.casefold()) in _KIND_BY_MANIFEST
89
+ }
90
+ if not matched:
91
+ return ComponentKind.UNKNOWN
92
+
93
+ return max(matched, key=lambda kind: (_KIND_PRIORITY[kind], kind.value))
94
+
95
+
96
+ class Enrichment(StrEnum):
97
+ """Deep-pass outcome; the only way to tell a missing field from a gap."""
98
+
99
+ ABSENT = "absent"
100
+ FAILED = "failed"
101
+ PRESENT = "present"
102
+
103
+
104
+ class Component(BaseModel):
105
+ model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid", strict=True)
106
+
107
+ path: RelativePath
108
+ name: Annotated[str, StringConstraints(min_length=1, max_length=512)]
109
+ kind: ComponentKind
110
+ enrichment: Enrichment
111
+ properties: dict[str, object] = Field(default_factory=dict)
@@ -0,0 +1,57 @@
1
+ import unicodedata
2
+ from enum import StrEnum
3
+ from typing import Annotated, ClassVar
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, StringConstraints, model_validator
6
+
7
+ from fluidattacks_core.contextualizes.components import Component
8
+ from fluidattacks_core.contextualizes.observations import Observation
9
+
10
+ SCHEMA_VERSION = 1
11
+
12
+
13
+ class RunStatus(StrEnum):
14
+ """How much of the repository the run covered.
15
+
16
+ A consumer reconciles by deleting what the output omits, so it must be
17
+ able to refuse anything but `complete`.
18
+ """
19
+
20
+ COMPLETE = "complete"
21
+ FAILED = "failed"
22
+ PARTIAL = "partial"
23
+
24
+
25
+ class Repository(BaseModel):
26
+ model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid", strict=True)
27
+
28
+ url: Annotated[str, StringConstraints(min_length=1, max_length=2048)]
29
+ branch: Annotated[str, StringConstraints(min_length=1, max_length=512)] | None = None
30
+
31
+
32
+ class ContextualizerOutput(BaseModel):
33
+ """One contextualizer's report over one repository.
34
+
35
+ Contextualizer vocabulary only: nothing about scope, groups or asset
36
+ types, none of which is observable from a bare checkout.
37
+ """
38
+
39
+ model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid", strict=True)
40
+
41
+ schema_version: int = Field(ge=1, le=SCHEMA_VERSION)
42
+ producer: Annotated[str, StringConstraints(min_length=1, max_length=64)]
43
+ repository: Repository
44
+ status: RunStatus
45
+ components: tuple[Component, ...] = ()
46
+ observations: tuple[Observation, ...] = ()
47
+
48
+ @model_validator(mode="after")
49
+ def _reject_duplicate_component_paths(self) -> "ContextualizerOutput":
50
+ # Folded: two normalization forms are two files here, but a consumer
51
+ # keying on the normalized path sees one, and loses an entry.
52
+ paths = [unicodedata.normalize("NFC", component.path) for component in self.components]
53
+ if len(set(paths)) != len(paths):
54
+ msg = "components must not repeat a path"
55
+ raise ValueError(msg)
56
+
57
+ return self
@@ -0,0 +1,28 @@
1
+ from enum import StrEnum
2
+ from typing import Annotated, ClassVar
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field, StringConstraints
5
+
6
+ from fluidattacks_core.contextualizes.paths import RelativePath
7
+
8
+
9
+ class ObservationKind(StrEnum):
10
+ AI_MODEL = "ai_model"
11
+ ENDPOINT = "endpoint"
12
+ PACKAGE = "package"
13
+
14
+
15
+ class Observation(BaseModel):
16
+ """Something found at a path, with no statement of which component owns it.
17
+
18
+ Unattributed so a producer need not run the partitioner, and a
19
+ repartition re-attributes everything with no rewrite here.
20
+ """
21
+
22
+ model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid", strict=True)
23
+
24
+ path: RelativePath
25
+ kind: ObservationKind
26
+ line: Annotated[int, Field(ge=1)] | None = None
27
+ properties: dict[str, object] = Field(default_factory=dict)
28
+ seen_by: Annotated[str, StringConstraints(min_length=1, max_length=128)] | None = None
@@ -0,0 +1,53 @@
1
+ import re
2
+ from typing import Annotated
3
+
4
+ from pydantic import AfterValidator, StringConstraints
5
+
6
+ MAX_PATH_LENGTH = 4096
7
+
8
+ REPOSITORY_ROOT = "."
9
+
10
+ # Anchored: a colon is legal in a posix file name.
11
+ _DRIVE_LETTER = re.compile(r"^[A-Za-z]:")
12
+ _FORBIDDEN = re.compile("[\\x00-\\x1f\\x7f\\u202a-\\u202e\\u2066-\\u2069]")
13
+
14
+
15
+ def ensure_relative_path(value: str) -> str:
16
+ """Reject, never normalize, anything but the canonical spelling of a path.
17
+
18
+ One spelling per path, so a consumer that folds `./a`, `a//b` and `a/` into
19
+ a single identity can never be handed two of them as separate entries.
20
+ """
21
+ if not value:
22
+ msg = "path must not be empty"
23
+ raise ValueError(msg)
24
+
25
+ if _FORBIDDEN.search(value):
26
+ msg = f"path must not hold control or bidi characters: {value!r}"
27
+ raise ValueError(msg)
28
+
29
+ if "\\" in value:
30
+ msg = f"path must use posix separators: {value!r}"
31
+ raise ValueError(msg)
32
+
33
+ if value.startswith("/") or _DRIVE_LETTER.match(value):
34
+ msg = f"path must be repository-relative: {value!r}"
35
+ raise ValueError(msg)
36
+
37
+ segments = value.split("/")
38
+ if any(segment == ".." for segment in segments):
39
+ msg = f"path must not traverse upwards: {value!r}"
40
+ raise ValueError(msg)
41
+
42
+ if value != REPOSITORY_ROOT and not all(segment and segment != "." for segment in segments):
43
+ msg = f"path must be in canonical form: {value!r}"
44
+ raise ValueError(msg)
45
+
46
+ return value
47
+
48
+
49
+ RelativePath = Annotated[
50
+ str,
51
+ StringConstraints(min_length=1, max_length=MAX_PATH_LENGTH),
52
+ AfterValidator(ensure_relative_path),
53
+ ]
File without changes
@@ -0,0 +1,35 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from fluidattacks_core.contextualizes.envelope import SCHEMA_VERSION, ContextualizerOutput
5
+
6
+ SCHEMA_PATH = Path(__file__).parent / "schemas" / "contextualizer_output.schema.json"
7
+
8
+ # Versioned: a validator caches by $id, so v2 must not answer to v1's uri.
9
+ _SCHEMA_ID = f"https://fluidattacks.com/schemas/contextualizer_output/v{SCHEMA_VERSION}.json"
10
+
11
+ # Declared, or a validator defaulting to draft-07 reads `$defs` differently.
12
+ _SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema"
13
+
14
+
15
+ def build_schema() -> dict[str, object]:
16
+ """Render the canonical JSONSchema the Rust producer validates against.
17
+
18
+ Structural only. Neither `ensure_relative_path` nor the unique-component
19
+ rule can be expressed here, so each producer implements both and the
20
+ fixtures are what prove the implementations agree.
21
+ """
22
+ schema = ContextualizerOutput.model_json_schema()
23
+
24
+ # Spread first, or a pydantic-emitted `$id` would silently win.
25
+ return {**schema, "$schema": _SCHEMA_DIALECT, "$id": _SCHEMA_ID}
26
+
27
+
28
+ def render_schema() -> str:
29
+ return json.dumps(build_schema(), indent=2, sort_keys=True) + "\n"
30
+
31
+
32
+ def load_schema() -> dict[str, object]:
33
+ content: dict[str, object] = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
34
+
35
+ return content
@@ -0,0 +1,220 @@
1
+ {
2
+ "$defs": {
3
+ "Component": {
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "enrichment": {
7
+ "$ref": "#/$defs/Enrichment"
8
+ },
9
+ "kind": {
10
+ "$ref": "#/$defs/ComponentKind"
11
+ },
12
+ "name": {
13
+ "maxLength": 512,
14
+ "minLength": 1,
15
+ "title": "Name",
16
+ "type": "string"
17
+ },
18
+ "path": {
19
+ "maxLength": 4096,
20
+ "minLength": 1,
21
+ "title": "Path",
22
+ "type": "string"
23
+ },
24
+ "properties": {
25
+ "additionalProperties": true,
26
+ "title": "Properties",
27
+ "type": "object"
28
+ }
29
+ },
30
+ "required": [
31
+ "path",
32
+ "name",
33
+ "kind",
34
+ "enrichment"
35
+ ],
36
+ "title": "Component",
37
+ "type": "object"
38
+ },
39
+ "ComponentKind": {
40
+ "description": "Ecosystem of a component, decided by a filename, never by a model.\n\nSpelled as the platform's package ecosystem ids so a component joins the\npackage inventory without a translation table. `gradle` and `sbt` have no\nid of their own there; both resolve from the maven registry.",
41
+ "enum": [
42
+ "cargo",
43
+ "go",
44
+ "gradle",
45
+ "maven",
46
+ "npm",
47
+ "nuget",
48
+ "packagist",
49
+ "pub",
50
+ "pypi",
51
+ "rubygems",
52
+ "sbt",
53
+ "swifturl",
54
+ "unknown"
55
+ ],
56
+ "title": "ComponentKind",
57
+ "type": "string"
58
+ },
59
+ "Enrichment": {
60
+ "description": "Deep-pass outcome; the only way to tell a missing field from a gap.",
61
+ "enum": [
62
+ "absent",
63
+ "failed",
64
+ "present"
65
+ ],
66
+ "title": "Enrichment",
67
+ "type": "string"
68
+ },
69
+ "Observation": {
70
+ "additionalProperties": false,
71
+ "description": "Something found at a path, with no statement of which component owns it.\n\nUnattributed so a producer need not run the partitioner, and a\nrepartition re-attributes everything with no rewrite here.",
72
+ "properties": {
73
+ "kind": {
74
+ "$ref": "#/$defs/ObservationKind"
75
+ },
76
+ "line": {
77
+ "anyOf": [
78
+ {
79
+ "minimum": 1,
80
+ "type": "integer"
81
+ },
82
+ {
83
+ "type": "null"
84
+ }
85
+ ],
86
+ "default": null,
87
+ "title": "Line"
88
+ },
89
+ "path": {
90
+ "maxLength": 4096,
91
+ "minLength": 1,
92
+ "title": "Path",
93
+ "type": "string"
94
+ },
95
+ "properties": {
96
+ "additionalProperties": true,
97
+ "title": "Properties",
98
+ "type": "object"
99
+ },
100
+ "seen_by": {
101
+ "anyOf": [
102
+ {
103
+ "maxLength": 128,
104
+ "minLength": 1,
105
+ "type": "string"
106
+ },
107
+ {
108
+ "type": "null"
109
+ }
110
+ ],
111
+ "default": null,
112
+ "title": "Seen By"
113
+ }
114
+ },
115
+ "required": [
116
+ "path",
117
+ "kind"
118
+ ],
119
+ "title": "Observation",
120
+ "type": "object"
121
+ },
122
+ "ObservationKind": {
123
+ "enum": [
124
+ "ai_model",
125
+ "endpoint",
126
+ "package"
127
+ ],
128
+ "title": "ObservationKind",
129
+ "type": "string"
130
+ },
131
+ "Repository": {
132
+ "additionalProperties": false,
133
+ "properties": {
134
+ "branch": {
135
+ "anyOf": [
136
+ {
137
+ "maxLength": 512,
138
+ "minLength": 1,
139
+ "type": "string"
140
+ },
141
+ {
142
+ "type": "null"
143
+ }
144
+ ],
145
+ "default": null,
146
+ "title": "Branch"
147
+ },
148
+ "url": {
149
+ "maxLength": 2048,
150
+ "minLength": 1,
151
+ "title": "Url",
152
+ "type": "string"
153
+ }
154
+ },
155
+ "required": [
156
+ "url"
157
+ ],
158
+ "title": "Repository",
159
+ "type": "object"
160
+ },
161
+ "RunStatus": {
162
+ "description": "How much of the repository the run covered.\n\nA consumer reconciles by deleting what the output omits, so it must be\nable to refuse anything but `complete`.",
163
+ "enum": [
164
+ "complete",
165
+ "failed",
166
+ "partial"
167
+ ],
168
+ "title": "RunStatus",
169
+ "type": "string"
170
+ }
171
+ },
172
+ "$id": "https://fluidattacks.com/schemas/contextualizer_output/v1.json",
173
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
174
+ "additionalProperties": false,
175
+ "description": "One contextualizer's report over one repository.\n\nContextualizer vocabulary only: nothing about scope, groups or asset\ntypes, none of which is observable from a bare checkout.",
176
+ "properties": {
177
+ "components": {
178
+ "default": [],
179
+ "items": {
180
+ "$ref": "#/$defs/Component"
181
+ },
182
+ "title": "Components",
183
+ "type": "array"
184
+ },
185
+ "observations": {
186
+ "default": [],
187
+ "items": {
188
+ "$ref": "#/$defs/Observation"
189
+ },
190
+ "title": "Observations",
191
+ "type": "array"
192
+ },
193
+ "producer": {
194
+ "maxLength": 64,
195
+ "minLength": 1,
196
+ "title": "Producer",
197
+ "type": "string"
198
+ },
199
+ "repository": {
200
+ "$ref": "#/$defs/Repository"
201
+ },
202
+ "schema_version": {
203
+ "maximum": 1,
204
+ "minimum": 1,
205
+ "title": "Schema Version",
206
+ "type": "integer"
207
+ },
208
+ "status": {
209
+ "$ref": "#/$defs/RunStatus"
210
+ }
211
+ },
212
+ "required": [
213
+ "schema_version",
214
+ "producer",
215
+ "repository",
216
+ "status"
217
+ ],
218
+ "title": "ContextualizerOutput",
219
+ "type": "object"
220
+ }
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.5
2
+ Name: fluidattacks_core_contextualizes
3
+ Version: 2.0.0
4
+ Summary: Fluid Attacks Core Contextualizes Library
5
+ Author-email: Development <development@fluidattacks.com>
6
+ License: MPL-2.0
7
+ Classifier: Development Status :: 1 - Planning
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: fluidattacks-core-filesystem<15,>=14.0.0
15
+ Requires-Dist: pydantic<3,>=2.12.3
@@ -0,0 +1,11 @@
1
+ fluidattacks_core/contextualizes/__init__.py,sha256=KnIpVThsTcVV76bWyx7rk4kDP2kHoIWet45KvkNiqs0,1063
2
+ fluidattacks_core/contextualizes/components.py,sha256=tMUxxrudm0Fh5-BE6DvfBsf5ZjeuqTyOg0lqzMFWYyg,3825
3
+ fluidattacks_core/contextualizes/envelope.py,sha256=egzBdg8pYsxkPGW_RfnBdlY23ZU1n_ArDfoDn8-L7jM,1985
4
+ fluidattacks_core/contextualizes/observations.py,sha256=ceQCVD5IDXlD34Lv7OdVGWNoscf82uGcQStlmSK1Bco,899
5
+ fluidattacks_core/contextualizes/paths.py,sha256=OeUX5MG3liuQWth286sMx_TJp9gOMPxbtN4ISXXlwqY,1617
6
+ fluidattacks_core/contextualizes/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ fluidattacks_core/contextualizes/schema.py,sha256=dbEAJ_trPyLONRsmbQCsUrpALYMwqhvTAsgdUco2iRk,1302
8
+ fluidattacks_core/contextualizes/schemas/contextualizer_output.schema.json,sha256=HiC0LmPRs7FshfjAyHjRlLJLI-T_NAMyL287p1_-p8s,5411
9
+ fluidattacks_core_contextualizes-2.0.0.dist-info/METADATA,sha256=jTgye4mDmUxB6Q8Dz_i_tiAs6G7hGzbsA-SFIDL1V9Q,637
10
+ fluidattacks_core_contextualizes-2.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
11
+ fluidattacks_core_contextualizes-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any