fluidattacks_core_contextualizes 2.0.0__tar.gz

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.
Files changed (40) hide show
  1. fluidattacks_core_contextualizes-2.0.0/.envrc +1 -0
  2. fluidattacks_core_contextualizes-2.0.0/.gitignore +9 -0
  3. fluidattacks_core_contextualizes-2.0.0/PKG-INFO +15 -0
  4. fluidattacks_core_contextualizes-2.0.0/build/default.nix +19 -0
  5. fluidattacks_core_contextualizes-2.0.0/build/filter.nix +13 -0
  6. fluidattacks_core_contextualizes-2.0.0/flake.lock +303 -0
  7. fluidattacks_core_contextualizes-2.0.0/flake.nix +29 -0
  8. fluidattacks_core_contextualizes-2.0.0/fluidattacks_core/contextualizes/__init__.py +48 -0
  9. fluidattacks_core_contextualizes-2.0.0/fluidattacks_core/contextualizes/components.py +111 -0
  10. fluidattacks_core_contextualizes-2.0.0/fluidattacks_core/contextualizes/envelope.py +57 -0
  11. fluidattacks_core_contextualizes-2.0.0/fluidattacks_core/contextualizes/observations.py +28 -0
  12. fluidattacks_core_contextualizes-2.0.0/fluidattacks_core/contextualizes/paths.py +53 -0
  13. fluidattacks_core_contextualizes-2.0.0/fluidattacks_core/contextualizes/py.typed +0 -0
  14. fluidattacks_core_contextualizes-2.0.0/fluidattacks_core/contextualizes/schema.py +35 -0
  15. fluidattacks_core_contextualizes-2.0.0/fluidattacks_core/contextualizes/schemas/contextualizer_output.schema.json +220 -0
  16. fluidattacks_core_contextualizes-2.0.0/import-linter.cfg +16 -0
  17. fluidattacks_core_contextualizes-2.0.0/mypy.ini +32 -0
  18. fluidattacks_core_contextualizes-2.0.0/pyproject.toml +42 -0
  19. fluidattacks_core_contextualizes-2.0.0/ruff.toml +45 -0
  20. fluidattacks_core_contextualizes-2.0.0/tests/__init__.py +0 -0
  21. fluidattacks_core_contextualizes-2.0.0/tests/data/discovers_output.json +41 -0
  22. fluidattacks_core_contextualizes-2.0.0/tests/data/explores_output.json +54 -0
  23. fluidattacks_core_contextualizes-2.0.0/tests/data/invalid_absolute_path.json +54 -0
  24. fluidattacks_core_contextualizes-2.0.0/tests/data/invalid_attributed_observation.json +55 -0
  25. fluidattacks_core_contextualizes-2.0.0/tests/data/invalid_backslash_path.json +54 -0
  26. fluidattacks_core_contextualizes-2.0.0/tests/data/invalid_coerced_schema_version.json +54 -0
  27. fluidattacks_core_contextualizes-2.0.0/tests/data/invalid_control_character_path.json +54 -0
  28. fluidattacks_core_contextualizes-2.0.0/tests/data/invalid_dot_segment_path.json +54 -0
  29. fluidattacks_core_contextualizes-2.0.0/tests/data/invalid_duplicate_component.json +67 -0
  30. fluidattacks_core_contextualizes-2.0.0/tests/data/invalid_duplicate_normalization.json +67 -0
  31. fluidattacks_core_contextualizes-2.0.0/tests/data/invalid_empty_segment_path.json +54 -0
  32. fluidattacks_core_contextualizes-2.0.0/tests/data/invalid_missing_enrichment.json +53 -0
  33. fluidattacks_core_contextualizes-2.0.0/tests/data/invalid_missing_status.json +53 -0
  34. fluidattacks_core_contextualizes-2.0.0/tests/data/invalid_trailing_separator_path.json +54 -0
  35. fluidattacks_core_contextualizes-2.0.0/tests/data/invalid_traversal_path.json +54 -0
  36. fluidattacks_core_contextualizes-2.0.0/tests/test_components.py +67 -0
  37. fluidattacks_core_contextualizes-2.0.0/tests/test_envelope.py +107 -0
  38. fluidattacks_core_contextualizes-2.0.0/tests/test_paths.py +50 -0
  39. fluidattacks_core_contextualizes-2.0.0/tests/test_schema.py +58 -0
  40. fluidattacks_core_contextualizes-2.0.0/uv.lock +1027 -0
@@ -0,0 +1 @@
1
+ use_flake ".#python311.devShell"
@@ -0,0 +1,9 @@
1
+ dist/
2
+ dist-*/
3
+ /.cargo-cache
4
+ target/
5
+ .direnv/
6
+ .venv/
7
+ .vscode/settings.json
8
+ .zed/settings.json
9
+ *.so
@@ -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,19 @@
1
+ { src, workspace_root }:
2
+ {
3
+ inherit src workspace_root;
4
+ root_path = "develops/fluidattacks-core/python/contextualizes";
5
+ module_name = "fluidattacks_core";
6
+ pypi_token = {
7
+ method = "oidc";
8
+ };
9
+ override = {
10
+ checks = {
11
+ basedpyright = false;
12
+ };
13
+ configs = {
14
+ import_linter = ../import-linter.cfg;
15
+ mypy = ../mypy.ini;
16
+ ruff = ../ruff.toml;
17
+ };
18
+ };
19
+ }
@@ -0,0 +1,13 @@
1
+ path_filter: src:
2
+ path_filter {
3
+ root = src;
4
+ include = [
5
+ "fluidattacks_core"
6
+ "tests"
7
+ "import-linter.cfg"
8
+ "mypy.ini"
9
+ "pyproject.toml"
10
+ "ruff.toml"
11
+ "uv.lock"
12
+ ];
13
+ }
@@ -0,0 +1,303 @@
1
+ {
2
+ "nodes": {
3
+ "flake-parts": {
4
+ "inputs": {
5
+ "nixpkgs-lib": "nixpkgs-lib"
6
+ },
7
+ "locked": {
8
+ "lastModified": 1762810396,
9
+ "narHash": "sha256-dxFVgQPG+R72dkhXTtqUm7KpxElw3u6E+YlQ2WaDgt8=",
10
+ "owner": "hercules-ci",
11
+ "repo": "flake-parts",
12
+ "rev": "0bdadb1b265fb4143a75bd1ec7d8c915898a9923",
13
+ "type": "github"
14
+ },
15
+ "original": {
16
+ "owner": "hercules-ci",
17
+ "repo": "flake-parts",
18
+ "type": "github"
19
+ }
20
+ },
21
+ "nix_filter": {
22
+ "locked": {
23
+ "lastModified": 1757882181,
24
+ "narHash": "sha256-+cCxYIh2UNalTz364p+QYmWHs0P+6wDhiWR4jDIKQIU=",
25
+ "owner": "numtide",
26
+ "repo": "nix-filter",
27
+ "rev": "59c44d1909c72441144b93cf0f054be7fe764de5",
28
+ "type": "github"
29
+ },
30
+ "original": {
31
+ "owner": "numtide",
32
+ "repo": "nix-filter",
33
+ "type": "github"
34
+ }
35
+ },
36
+ "nixpkgs": {
37
+ "locked": {
38
+ "lastModified": 1758690382,
39
+ "narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
40
+ "owner": "nixos",
41
+ "repo": "nixpkgs",
42
+ "rev": "e643668fd71b949c53f8626614b21ff71a07379d",
43
+ "type": "github"
44
+ },
45
+ "original": {
46
+ "owner": "nixos",
47
+ "ref": "nixos-unstable",
48
+ "repo": "nixpkgs",
49
+ "type": "github"
50
+ }
51
+ },
52
+ "nixpkgs-lib": {
53
+ "locked": {
54
+ "lastModified": 1761765539,
55
+ "narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=",
56
+ "owner": "nix-community",
57
+ "repo": "nixpkgs.lib",
58
+ "rev": "719359f4562934ae99f5443f20aa06c2ffff91fc",
59
+ "type": "github"
60
+ },
61
+ "original": {
62
+ "owner": "nix-community",
63
+ "repo": "nixpkgs.lib",
64
+ "type": "github"
65
+ }
66
+ },
67
+ "nixpkgs_2": {
68
+ "locked": {
69
+ "lastModified": 1758690382,
70
+ "narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
71
+ "owner": "nixos",
72
+ "repo": "nixpkgs",
73
+ "rev": "e643668fd71b949c53f8626614b21ff71a07379d",
74
+ "type": "github"
75
+ },
76
+ "original": {
77
+ "owner": "nixos",
78
+ "ref": "nixos-unstable",
79
+ "repo": "nixpkgs",
80
+ "type": "github"
81
+ }
82
+ },
83
+ "nixpkgs_3": {
84
+ "locked": {
85
+ "lastModified": 1761373498,
86
+ "narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=",
87
+ "owner": "nixos",
88
+ "repo": "nixpkgs",
89
+ "rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce",
90
+ "type": "github"
91
+ },
92
+ "original": {
93
+ "owner": "nixos",
94
+ "ref": "nixos-unstable",
95
+ "repo": "nixpkgs",
96
+ "type": "github"
97
+ }
98
+ },
99
+ "nixpkgs_flake": {
100
+ "locked": {
101
+ "lastModified": 1778253243,
102
+ "narHash": "sha256-KkH2QvU6iRLDTGGEwH5jnMqHWj4zMTuHxi1S1zdr01w=",
103
+ "owner": "nixos",
104
+ "repo": "nixpkgs",
105
+ "rev": "4fcda7103530b23c5ca44286344912082aad4220",
106
+ "type": "github"
107
+ },
108
+ "original": {
109
+ "owner": "nixos",
110
+ "repo": "nixpkgs",
111
+ "type": "github"
112
+ }
113
+ },
114
+ "pyproject-build-systems": {
115
+ "inputs": {
116
+ "nixpkgs": "nixpkgs",
117
+ "pyproject-nix": "pyproject-nix",
118
+ "uv2nix": "uv2nix"
119
+ },
120
+ "locked": {
121
+ "lastModified": 1761781027,
122
+ "narHash": "sha256-YDvxPAm2WnxrznRqWwHLjryBGG5Ey1ATEJXrON+TWt8=",
123
+ "owner": "pyproject-nix",
124
+ "repo": "build-system-pkgs",
125
+ "rev": "795a980d25301e5133eca37adae37283ec3c8e66",
126
+ "type": "github"
127
+ },
128
+ "original": {
129
+ "owner": "pyproject-nix",
130
+ "repo": "build-system-pkgs",
131
+ "type": "github"
132
+ }
133
+ },
134
+ "pyproject-nix": {
135
+ "inputs": {
136
+ "nixpkgs": [
137
+ "python_builder",
138
+ "pyproject-build-systems",
139
+ "nixpkgs"
140
+ ]
141
+ },
142
+ "locked": {
143
+ "lastModified": 1758265079,
144
+ "narHash": "sha256-amLaLNwKSZPShQHzfgmc/9o76dU8xzN0743dWgvYlr8=",
145
+ "owner": "nix-community",
146
+ "repo": "pyproject.nix",
147
+ "rev": "02e9418fd4af638447dca4b17b1280da95527fc9",
148
+ "type": "github"
149
+ },
150
+ "original": {
151
+ "owner": "nix-community",
152
+ "repo": "pyproject.nix",
153
+ "type": "github"
154
+ }
155
+ },
156
+ "pyproject-nix_2": {
157
+ "inputs": {
158
+ "nixpkgs": "nixpkgs_2"
159
+ },
160
+ "locked": {
161
+ "lastModified": 1762427963,
162
+ "narHash": "sha256-CkPlAbIQ87wmjy5qHibfzk4DmMGBNqFer+lLfXjpP5M=",
163
+ "owner": "pyproject-nix",
164
+ "repo": "pyproject.nix",
165
+ "rev": "4540ea004e04fcd12dd2738d51383d10f956f7b9",
166
+ "type": "github"
167
+ },
168
+ "original": {
169
+ "owner": "pyproject-nix",
170
+ "repo": "pyproject.nix",
171
+ "type": "github"
172
+ }
173
+ },
174
+ "pyproject-nix_3": {
175
+ "inputs": {
176
+ "nixpkgs": [
177
+ "python_builder",
178
+ "uv2nix",
179
+ "nixpkgs"
180
+ ]
181
+ },
182
+ "locked": {
183
+ "lastModified": 1760402624,
184
+ "narHash": "sha256-jF6UKLs2uGc2rtved8Vrt58oTWjTQoAssuYs/0578Z4=",
185
+ "owner": "pyproject-nix",
186
+ "repo": "pyproject.nix",
187
+ "rev": "84c4ea102127c77058ea1ed7be7300261fafc7d2",
188
+ "type": "github"
189
+ },
190
+ "original": {
191
+ "owner": "pyproject-nix",
192
+ "repo": "pyproject.nix",
193
+ "type": "github"
194
+ }
195
+ },
196
+ "python_builder": {
197
+ "inputs": {
198
+ "nix_filter": "nix_filter",
199
+ "nixpkgs_flake": "nixpkgs_flake",
200
+ "pyproject-build-systems": "pyproject-build-systems",
201
+ "pyproject-nix": "pyproject-nix_2",
202
+ "shell-helpers": "shell-helpers",
203
+ "uv2nix": "uv2nix_2"
204
+ },
205
+ "locked": {
206
+ "dir": "common/utils/python-builder",
207
+ "lastModified": 1787078362,
208
+ "narHash": "sha256-HYL7Rn6SuNjYxkt/PwFLksZjsauCiY+ENbOWryLsIlw=",
209
+ "ref": "refs/heads/trunk",
210
+ "rev": "09fd25a13309eb5e3b5b2a061bb340df3ae5d281",
211
+ "shallow": true,
212
+ "type": "git",
213
+ "url": "ssh://git@gitlab.com/fluidattacks/universe"
214
+ },
215
+ "original": {
216
+ "dir": "common/utils/python-builder",
217
+ "rev": "09fd25a13309eb5e3b5b2a061bb340df3ae5d281",
218
+ "shallow": true,
219
+ "type": "git",
220
+ "url": "ssh://git@gitlab.com/fluidattacks/universe"
221
+ }
222
+ },
223
+ "root": {
224
+ "inputs": {
225
+ "python_builder": "python_builder"
226
+ }
227
+ },
228
+ "shell-helpers": {
229
+ "inputs": {
230
+ "flake-parts": "flake-parts",
231
+ "nixpkgs": [
232
+ "python_builder",
233
+ "nixpkgs_flake"
234
+ ]
235
+ },
236
+ "locked": {
237
+ "dir": "common/utils/shell-helpers",
238
+ "lastModified": 1762806066,
239
+ "narHash": "sha256-Wz99Fl7SpPU1NkTHJ36XOo6DjOEAMXCw3ThRABOS2Hs=",
240
+ "ref": "refs/heads/trunk",
241
+ "rev": "d73cfdc7ae9e7f547e22a4481d4c2ccce38b26a8",
242
+ "shallow": true,
243
+ "type": "git",
244
+ "url": "ssh://git@gitlab.com/fluidattacks/universe"
245
+ },
246
+ "original": {
247
+ "dir": "common/utils/shell-helpers",
248
+ "rev": "d73cfdc7ae9e7f547e22a4481d4c2ccce38b26a8",
249
+ "shallow": true,
250
+ "type": "git",
251
+ "url": "ssh://git@gitlab.com/fluidattacks/universe"
252
+ }
253
+ },
254
+ "uv2nix": {
255
+ "inputs": {
256
+ "nixpkgs": [
257
+ "python_builder",
258
+ "pyproject-build-systems",
259
+ "nixpkgs"
260
+ ],
261
+ "pyproject-nix": [
262
+ "python_builder",
263
+ "pyproject-build-systems",
264
+ "pyproject-nix"
265
+ ]
266
+ },
267
+ "locked": {
268
+ "lastModified": 1758933732,
269
+ "narHash": "sha256-HAmm1GBS1myZCFuog0DC2ZLaynvZtiUI2Crmo+cdQI0=",
270
+ "owner": "pyproject-nix",
271
+ "repo": "uv2nix",
272
+ "rev": "273ce18f913d8559e0d04f820d724308966d7c4d",
273
+ "type": "github"
274
+ },
275
+ "original": {
276
+ "owner": "pyproject-nix",
277
+ "repo": "uv2nix",
278
+ "type": "github"
279
+ }
280
+ },
281
+ "uv2nix_2": {
282
+ "inputs": {
283
+ "nixpkgs": "nixpkgs_3",
284
+ "pyproject-nix": "pyproject-nix_3"
285
+ },
286
+ "locked": {
287
+ "lastModified": 1761872265,
288
+ "narHash": "sha256-i25GRgp2vUOebY70L3NTAgkd+Pr1hnn5xM3qHxH0ONU=",
289
+ "owner": "pyproject-nix",
290
+ "repo": "uv2nix",
291
+ "rev": "74dfb62871be152ad3673b143b0cc56105a4f3c5",
292
+ "type": "github"
293
+ },
294
+ "original": {
295
+ "owner": "pyproject-nix",
296
+ "repo": "uv2nix",
297
+ "type": "github"
298
+ }
299
+ }
300
+ },
301
+ "root": "root",
302
+ "version": 7
303
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ description = "Fluid Attacks Core contextualizes package.";
3
+
4
+ inputs = {
5
+ python_builder = {
6
+ url = "git+ssh://git@gitlab.com/fluidattacks/universe?shallow=1&rev=09fd25a13309eb5e3b5b2a061bb340df3ae5d281&dir=common/utils/python-builder";
7
+ };
8
+ };
9
+
10
+ outputs =
11
+ { self, ... }@inputs:
12
+ let
13
+ build_args =
14
+ {
15
+ system,
16
+ python_version,
17
+ nixpkgs,
18
+ builders,
19
+ scripts,
20
+ }:
21
+ import ./build {
22
+ src = import ./build/filter.nix nixpkgs.nix-filter self;
23
+ workspace_root = ./.;
24
+ };
25
+ in
26
+ {
27
+ packages = inputs.python_builder.outputs.build build_args;
28
+ };
29
+ }
@@ -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