fluidattacks_core_models 12.0.0__tar.gz → 13.1.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 (36) hide show
  1. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/.gitignore +2 -0
  2. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/PKG-INFO +1 -1
  3. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/state_machines/__init__.py +6 -0
  4. fluidattacks_core_models-13.1.0/fluidattacks_core/models/state_machines/component_orchestrator.py +59 -0
  5. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/state_machines/root_scanner.py +1 -2
  6. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/import-linter.cfg +1 -10
  7. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/pyproject.toml +1 -1
  8. fluidattacks_core_models-13.1.0/tests/test_component_orchestrator.py +169 -0
  9. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/tests/test_root_scanner.py +2 -38
  10. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/uv.lock +1 -1
  11. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/.envrc +0 -0
  12. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/build/default.nix +0 -0
  13. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/build/filter.nix +0 -0
  14. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/flake.lock +0 -0
  15. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/flake.nix +0 -0
  16. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/__init__.py +0 -0
  17. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/py.typed +0 -0
  18. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/scanners/__init__.py +0 -0
  19. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/scanners/apk_config.py +0 -0
  20. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/scanners/common_config.py +0 -0
  21. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/scanners/containers_sca_config.py +0 -0
  22. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/scanners/cspm_config.py +0 -0
  23. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/scanners/dast_config.py +0 -0
  24. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/scanners/loader.py +0 -0
  25. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/scanners/sast_config.py +0 -0
  26. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/scanners/sbom_config.py +0 -0
  27. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/scanners/sca_config.py +0 -0
  28. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/scanners/scan_config.py +0 -0
  29. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/scanners/sniffs_config.py +0 -0
  30. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/fluidattacks_core/models/state_machines/types.py +0 -0
  31. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/mypy.ini +0 -0
  32. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/ruff.toml +0 -0
  33. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/tests/__init__.py +0 -0
  34. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/tests/test_loader_env.py +0 -0
  35. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/tests/test_model_scanner.py +0 -0
  36. {fluidattacks_core_models-12.0.0 → fluidattacks_core_models-13.1.0}/tests/test_state_machines.py +0 -0
@@ -1,3 +1,5 @@
1
+ dist-*/
2
+ /.cargo-cache
1
3
  target/
2
4
  .direnv/
3
5
  .venv/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fluidattacks_core_models
3
- Version: 12.0.0
3
+ Version: 13.1.0
4
4
  Summary: Fluid Attacks Core Models Library
5
5
  Author-email: Development <development@fluidattacks.com>
6
6
  License: MPL-2.0
@@ -1,3 +1,7 @@
1
+ from fluidattacks_core.models.state_machines.component_orchestrator import (
2
+ ComponentScanOptions,
3
+ StateMachineComponentOrchestrator,
4
+ )
1
5
  from fluidattacks_core.models.state_machines.root_scanner import (
2
6
  ContextualizesScanInput,
3
7
  ContextualizesScanOptions,
@@ -18,11 +22,13 @@ from fluidattacks_core.models.state_machines.types import (
18
22
  )
19
23
 
20
24
  __all__ = [
25
+ "ComponentScanOptions",
21
26
  "ContextualizesScanInput",
22
27
  "ContextualizesScanOptions",
23
28
  "PerRootScanOptions",
24
29
  "StateMachine",
25
30
  "StateMachineCloningInput",
31
+ "StateMachineComponentOrchestrator",
26
32
  "StateMachineExecutePerRootInput",
27
33
  "StateMachineExecuteScannerInput",
28
34
  "StateMachineExecuteSiftsInput",
@@ -0,0 +1,59 @@
1
+ from hashlib import sha256
2
+ from typing import Generic, TypeVar
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field, computed_field
5
+
6
+ from fluidattacks_core.models.state_machines.root_scanner import (
7
+ MACHINE_NAME_DIGEST_LENGTH,
8
+ MAX_EXECUTION_NAME,
9
+ UNSAFE_NAME_CHARS,
10
+ TJobNameSegment,
11
+ TStorageKey,
12
+ TVisibleAscii,
13
+ )
14
+
15
+
16
+ class ComponentScanOptions(BaseModel):
17
+ model_config = ConfigDict(extra="forbid", frozen=True, revalidate_instances="always")
18
+ config_id: TStorageKey
19
+ entity_name: TStorageKey
20
+ user_email: TVisibleAscii = Field(..., min_length=1)
21
+
22
+
23
+ TComponentOptions = TypeVar("TComponentOptions", bound=ComponentScanOptions)
24
+
25
+
26
+ class StateMachineComponentOrchestrator(BaseModel, Generic[TComponentOptions]):
27
+ model_config = ConfigDict(extra="forbid", frozen=True, serialize_by_alias=True)
28
+ job_definition: str = Field(..., min_length=1, serialization_alias="jobDefinition")
29
+ job_queue: str = Field(..., min_length=1, serialization_alias="jobQueue")
30
+ scanner_name: TJobNameSegment = Field(..., serialization_alias="scannerName")
31
+ options: TComponentOptions = Field(..., exclude=True)
32
+
33
+ @computed_field(alias="configId") # type: ignore[prop-decorator]
34
+ @property
35
+ def config_id(self) -> str:
36
+ return self.options.config_id
37
+
38
+ @computed_field(alias="entityName") # type: ignore[prop-decorator]
39
+ @property
40
+ def entity_name(self) -> str:
41
+ return self.options.entity_name
42
+
43
+ @computed_field(alias="executionOptions") # type: ignore[prop-decorator]
44
+ @property
45
+ def execution_options(self) -> str:
46
+ return self.options.model_dump_json()
47
+
48
+ def to_wire(self) -> dict[str, str]:
49
+ return self.model_dump(mode="json", by_alias=True)
50
+
51
+ def get_machine_name(self) -> str:
52
+ digest = sha256(
53
+ (
54
+ f"{self.options.entity_name}\x00{self.scanner_name}\x00{self.options.config_id}"
55
+ ).encode()
56
+ ).hexdigest()[:MACHINE_NAME_DIGEST_LENGTH]
57
+ readable = UNSAFE_NAME_CHARS.sub("_", f"{self.options.entity_name}_{self.scanner_name}")
58
+ budget = MAX_EXECUTION_NAME - MACHINE_NAME_DIGEST_LENGTH - 1
59
+ return f"{readable[:budget]}_{digest}"
@@ -45,8 +45,7 @@ class ContextualizesScanOptions(PerRootScanOptions):
45
45
 
46
46
 
47
47
  class TracesScanOptions(PerRootScanOptions):
48
- root_id: TStorageKey
49
- commit: Annotated[str, StringConstraints(pattern=r"^[0-9a-f]{40}$")]
48
+ pass
50
49
 
51
50
 
52
51
  TOptions = TypeVar("TOptions", bound=PerRootScanOptions)
@@ -1,16 +1,7 @@
1
1
  [importlinter]
2
- root_package = fluidattacks_core
2
+ root_package = fluidattacks_core.models
3
3
  include_external_packages = True
4
4
 
5
- [importlinter:contract:portion]
6
- name = Single Portion
7
- type = layers
8
- containers =
9
- fluidattacks_core
10
- exhaustive = true
11
- layers =
12
- models
13
-
14
5
  [importlinter:contract:dag]
15
6
  name = Direct Acyclic Graph
16
7
  type = layers
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fluidattacks_core_models"
3
- version = "12.0.0"
3
+ version = "13.1.0"
4
4
  description = "Fluid Attacks Core Models Library"
5
5
  authors = [{ name = "Development", email = "development@fluidattacks.com" }]
6
6
  license = { text = "MPL-2.0" }
@@ -0,0 +1,169 @@
1
+ import json
2
+
3
+ import pytest
4
+ from pydantic import ValidationError
5
+
6
+ from fluidattacks_core.models.state_machines.component_orchestrator import (
7
+ ComponentScanOptions,
8
+ StateMachineComponentOrchestrator,
9
+ )
10
+ from fluidattacks_core.models.state_machines.root_scanner import (
11
+ MACHINE_NAME_DIGEST_LENGTH,
12
+ MAX_EXECUTION_NAME,
13
+ UNSAFE_NAME_CHARS,
14
+ )
15
+
16
+
17
+ class _ResolvesScanOptions(ComponentScanOptions):
18
+ seed_domain: str
19
+
20
+
21
+ def _options(**overrides: str) -> ComponentScanOptions:
22
+ arguments: dict[str, str] = {
23
+ "config_id": "123456",
24
+ "entity_name": "acme",
25
+ "user_email": "test@fluidattacks.com",
26
+ }
27
+ arguments.update(overrides)
28
+ return ComponentScanOptions(**arguments)
29
+
30
+
31
+ def _wire_payload(**overrides: object) -> dict[str, object]:
32
+ payload: dict[str, object] = {
33
+ "job_definition": "resolves_nix",
34
+ "job_queue": "resolves",
35
+ "scanner_name": "resolves-scan",
36
+ "options": _options(),
37
+ }
38
+ payload.update(overrides)
39
+ return payload
40
+
41
+
42
+ def _machine_input(
43
+ options: ComponentScanOptions | None = None,
44
+ scanner_name: str = "resolves-scan",
45
+ ) -> StateMachineComponentOrchestrator[ComponentScanOptions]:
46
+ resolved = options if options is not None else _options()
47
+ return StateMachineComponentOrchestrator[ComponentScanOptions].model_validate(
48
+ _wire_payload(options=resolved, scanner_name=scanner_name)
49
+ )
50
+
51
+
52
+ def test_orchestrator_serializes_six_wire_keys_from_the_options() -> None:
53
+ options = _options()
54
+ machine_input = _machine_input(options)
55
+ dumped = machine_input.model_dump(mode="json")
56
+ assert machine_input.to_wire() == dumped
57
+ assert set(dumped) == {
58
+ "configId",
59
+ "entityName",
60
+ "executionOptions",
61
+ "jobDefinition",
62
+ "jobQueue",
63
+ "scannerName",
64
+ }
65
+ blob = json.loads(dumped["executionOptions"])
66
+ assert dumped["configId"] == blob["config_id"]
67
+ assert dumped["entityName"] == blob["entity_name"]
68
+ assert ComponentScanOptions.model_validate_json(machine_input.execution_options) == options
69
+
70
+
71
+ def test_entity_name_is_a_storage_key_not_a_free_string() -> None:
72
+ for value in ("-h", "--storage-dir", ".", "..", "a..b", "_internal", "a/b", ""):
73
+ with pytest.raises(ValidationError):
74
+ _options(entity_name=value)
75
+ for value in ("acme\n", "acme\t", "acme corp", "gr\u043eup"):
76
+ with pytest.raises(ValidationError):
77
+ _options(entity_name=value)
78
+ for value in ("acme", "acme_nickname", "group.repo-name_v2", "0abc"):
79
+ assert _options(entity_name=value).entity_name == value
80
+
81
+
82
+ def test_options_refuse_unknown_fields_rebinding_and_unvalidated_instances() -> None:
83
+ with pytest.raises(ValidationError):
84
+ ComponentScanOptions.model_validate({**_options().model_dump(), "cwd": "../../.."})
85
+ options = _options()
86
+ for field in ("config_id", "entity_name", "user_email"):
87
+ with pytest.raises(ValidationError, match="frozen"):
88
+ setattr(options, field, "other")
89
+ smuggled = ComponentScanOptions.model_construct(
90
+ config_id="1", entity_name="../../etc", user_email="e@f"
91
+ )
92
+ with pytest.raises(ValidationError):
93
+ StateMachineComponentOrchestrator[ComponentScanOptions].model_validate(
94
+ _wire_payload(options=smuggled)
95
+ )
96
+
97
+
98
+ def test_orchestrator_rejects_unknown_and_empty_wire_fields() -> None:
99
+ for overrides in ({"unknown_field": "bad"}, {"job_definition": ""}, {"job_queue": ""}):
100
+ with pytest.raises(ValidationError):
101
+ StateMachineComponentOrchestrator[ComponentScanOptions].model_validate(
102
+ _wire_payload(**overrides)
103
+ )
104
+ for name in ("has space", "has.dot", "-h", "_leading", "sla/sh", ""):
105
+ with pytest.raises(ValidationError):
106
+ _machine_input(scanner_name=name)
107
+
108
+
109
+ def test_machine_name_stays_valid_at_the_execution_name_limit() -> None:
110
+ longest = _machine_input(
111
+ options=_options(config_id="c" * 128, entity_name="a" * 128),
112
+ scanner_name="s" * 200,
113
+ )
114
+ garbage = ComponentScanOptions.model_construct(
115
+ config_id="c" * 200, entity_name="a corp/../etc", user_email="e@f"
116
+ )
117
+ unvalidated = StateMachineComponentOrchestrator[ComponentScanOptions].model_construct(
118
+ job_definition="resolves_nix",
119
+ job_queue="resolves",
120
+ scanner_name="resolves scan/../x",
121
+ options=garbage,
122
+ )
123
+ for machine_input in (longest, unvalidated):
124
+ name = machine_input.get_machine_name()
125
+ assert len(name) <= MAX_EXECUTION_NAME
126
+ assert not UNSAFE_NAME_CHARS.search(name)
127
+
128
+
129
+ def test_machine_name_stays_greppable_by_entity_and_scanner() -> None:
130
+ assert _machine_input().get_machine_name().startswith("acme_resolves-scan_")
131
+
132
+
133
+ def test_machine_name_is_injective() -> None:
134
+ baseline = _machine_input().get_machine_name()
135
+ assert baseline != _machine_input(scanner_name="resolves-dast-scan").get_machine_name()
136
+ for field in ("config_id", "entity_name"):
137
+ assert baseline != _machine_input(_options(**{field: "other"})).get_machine_name()
138
+ hidden = "e" * (MAX_EXECUTION_NAME - MACHINE_NAME_DIGEST_LENGTH)
139
+ entities = {
140
+ _machine_input(_options(entity_name=f"{hidden}{suffix}")).get_machine_name()
141
+ for suffix in ("a", "b")
142
+ }
143
+ masked = {
144
+ _machine_input(_options(entity_name=hidden), scanner).get_machine_name()
145
+ for scanner in ("resolves-scan", "resolves-dast-scan")
146
+ }
147
+ filled = "a" * (MAX_EXECUTION_NAME - MACHINE_NAME_DIGEST_LENGTH - 1)
148
+ fenced = {
149
+ _machine_input(_options(entity_name=f"{filled}{tail}"), scanner).get_machine_name()
150
+ for tail, scanner in (("bc", "d"), ("b", "cd"))
151
+ }
152
+ assert len(entities) == 2
153
+ assert len(masked) == 2
154
+ assert len(fenced) == 2
155
+
156
+
157
+ def test_orchestrator_binds_the_options_type_it_is_parameterized_with() -> None:
158
+ resolves = _ResolvesScanOptions(
159
+ config_id="123456",
160
+ entity_name="acme",
161
+ seed_domain="acme.com",
162
+ user_email="test@fluidattacks.com",
163
+ )
164
+ machine_input = StateMachineComponentOrchestrator[_ResolvesScanOptions].model_validate(
165
+ _wire_payload(options=resolves)
166
+ )
167
+ assert json.loads(machine_input.execution_options)["seed_domain"] == "acme.com"
168
+ with pytest.raises(ValidationError):
169
+ StateMachineComponentOrchestrator[_ResolvesScanOptions].model_validate(_wire_payload())
@@ -15,8 +15,6 @@ from fluidattacks_core.models.state_machines.root_scanner import (
15
15
  TracesScanOptions,
16
16
  )
17
17
 
18
- _SHA = "0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c"
19
-
20
18
 
21
19
  class _DastScanOptions(PerRootScanOptions):
22
20
  network_name: str
@@ -277,8 +275,6 @@ def _traces_options(**overrides: str) -> TracesScanOptions:
277
275
  "group_name": "acme",
278
276
  "root_nickname": "nickname",
279
277
  "user_email": "test@fluidattacks.com",
280
- "root_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
281
- "commit": _SHA,
282
278
  }
283
279
  arguments.update(overrides)
284
280
  return TracesScanOptions(**arguments)
@@ -295,36 +291,12 @@ def _traces_input(options: TracesScanOptions | None = None) -> TracesScanInput:
295
291
  )
296
292
 
297
293
 
298
- def test_traces_scan_options_add_the_two_fields_the_artifact_layout_needs() -> None:
294
+ def test_traces_scan_options_are_exactly_the_shared_per_root_fields() -> None:
299
295
  options = _traces_options()
300
- assert set(options.model_dump()) == {
301
- "commit",
302
- "config_id",
303
- "group_name",
304
- "root_id",
305
- "root_nickname",
306
- "user_email",
307
- }
296
+ assert set(options.model_dump()) == set(PerRootScanOptions.model_fields)
308
297
  assert TracesScanOptions.model_validate_json(options.model_dump_json()) == options
309
298
 
310
299
 
311
- def test_traces_scan_options_reject_a_root_id_that_would_escape_its_prefix() -> None:
312
- for value in ("..", "a/b", "-h", ".", "a..b", "", "_internal"):
313
- with pytest.raises(ValidationError):
314
- _traces_options(root_id=value)
315
-
316
-
317
- def test_traces_scan_options_reject_a_commit_that_is_not_a_full_lowercase_sha() -> None:
318
- for value in ("", _SHA[:39], f"{_SHA}0", _SHA.upper(), _SHA.replace("0", "g"), f"{_SHA}\n"):
319
- with pytest.raises(ValidationError):
320
- _traces_options(commit=value)
321
-
322
-
323
- def test_traces_scan_options_accept_the_commits_the_mirror_reports() -> None:
324
- for value in (_SHA, "0" * 40, "f" * 40):
325
- assert _traces_options(commit=value).commit == value
326
-
327
-
328
300
  def test_traces_scan_input_keeps_the_seven_wire_keys_the_machine_reads() -> None:
329
301
  dumped = _traces_input().to_wire()
330
302
  assert set(dumped) == {
@@ -336,11 +308,3 @@ def test_traces_scan_input_keeps_the_seven_wire_keys_the_machine_reads() -> None
336
308
  "rootNickname",
337
309
  "scannerName",
338
310
  }
339
-
340
-
341
- def test_traces_scan_input_carries_root_id_and_commit_only_in_execution_options() -> None:
342
- options = _traces_options()
343
- dumped = _traces_input(options).to_wire()
344
- blob = dumped["executionOptions"]
345
- assert isinstance(blob, str)
346
- assert json.loads(blob) == options.model_dump()
@@ -187,7 +187,7 @@ wheels = [
187
187
 
188
188
  [[package]]
189
189
  name = "fluidattacks-core-models"
190
- version = "12.0.0"
190
+ version = "13.1.0"
191
191
  source = { editable = "." }
192
192
  dependencies = [
193
193
  { name = "pydantic" },