fluidattacks_core_models 13.0.0__tar.gz → 13.2.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.
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/.gitignore +4 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/PKG-INFO +1 -1
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/state_machines/__init__.py +8 -0
- fluidattacks_core_models-13.2.0/fluidattacks_core/models/state_machines/component_orchestrator.py +63 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/import-linter.cfg +1 -10
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/pyproject.toml +1 -1
- fluidattacks_core_models-13.2.0/tests/test_component_orchestrator.py +178 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/uv.lock +1 -1
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/.envrc +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/build/default.nix +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/build/filter.nix +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/flake.lock +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/flake.nix +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/__init__.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/py.typed +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/scanners/__init__.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/scanners/apk_config.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/scanners/common_config.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/scanners/containers_sca_config.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/scanners/cspm_config.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/scanners/dast_config.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/scanners/loader.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/scanners/sast_config.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/scanners/sbom_config.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/scanners/sca_config.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/scanners/scan_config.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/scanners/sniffs_config.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/state_machines/root_scanner.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/fluidattacks_core/models/state_machines/types.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/mypy.ini +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/ruff.toml +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/tests/__init__.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/tests/test_loader_env.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/tests/test_model_scanner.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/tests/test_root_scanner.py +0 -0
- {fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/tests/test_state_machines.py +0 -0
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
from fluidattacks_core.models.state_machines.component_orchestrator import (
|
|
2
|
+
ComponentScanOptions,
|
|
3
|
+
ResolvesScanOptions,
|
|
4
|
+
StateMachineComponentOrchestrator,
|
|
5
|
+
)
|
|
1
6
|
from fluidattacks_core.models.state_machines.root_scanner import (
|
|
2
7
|
ContextualizesScanInput,
|
|
3
8
|
ContextualizesScanOptions,
|
|
@@ -18,11 +23,14 @@ from fluidattacks_core.models.state_machines.types import (
|
|
|
18
23
|
)
|
|
19
24
|
|
|
20
25
|
__all__ = [
|
|
26
|
+
"ComponentScanOptions",
|
|
21
27
|
"ContextualizesScanInput",
|
|
22
28
|
"ContextualizesScanOptions",
|
|
23
29
|
"PerRootScanOptions",
|
|
30
|
+
"ResolvesScanOptions",
|
|
24
31
|
"StateMachine",
|
|
25
32
|
"StateMachineCloningInput",
|
|
33
|
+
"StateMachineComponentOrchestrator",
|
|
26
34
|
"StateMachineExecutePerRootInput",
|
|
27
35
|
"StateMachineExecuteScannerInput",
|
|
28
36
|
"StateMachineExecuteSiftsInput",
|
fluidattacks_core_models-13.2.0/fluidattacks_core/models/state_machines/component_orchestrator.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
class ResolvesScanOptions(ComponentScanOptions):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
TComponentOptions = TypeVar("TComponentOptions", bound=ComponentScanOptions)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StateMachineComponentOrchestrator(BaseModel, Generic[TComponentOptions]):
|
|
31
|
+
model_config = ConfigDict(extra="forbid", frozen=True, serialize_by_alias=True)
|
|
32
|
+
job_definition: str = Field(..., min_length=1, serialization_alias="jobDefinition")
|
|
33
|
+
job_queue: str = Field(..., min_length=1, serialization_alias="jobQueue")
|
|
34
|
+
scanner_name: TJobNameSegment = Field(..., serialization_alias="scannerName")
|
|
35
|
+
options: TComponentOptions = Field(..., exclude=True)
|
|
36
|
+
|
|
37
|
+
@computed_field(alias="configId") # type: ignore[prop-decorator]
|
|
38
|
+
@property
|
|
39
|
+
def config_id(self) -> str:
|
|
40
|
+
return self.options.config_id
|
|
41
|
+
|
|
42
|
+
@computed_field(alias="entityName") # type: ignore[prop-decorator]
|
|
43
|
+
@property
|
|
44
|
+
def entity_name(self) -> str:
|
|
45
|
+
return self.options.entity_name
|
|
46
|
+
|
|
47
|
+
@computed_field(alias="executionOptions") # type: ignore[prop-decorator]
|
|
48
|
+
@property
|
|
49
|
+
def execution_options(self) -> str:
|
|
50
|
+
return self.options.model_dump_json()
|
|
51
|
+
|
|
52
|
+
def to_wire(self) -> dict[str, str]:
|
|
53
|
+
return self.model_dump(mode="json", by_alias=True)
|
|
54
|
+
|
|
55
|
+
def get_machine_name(self) -> str:
|
|
56
|
+
digest = sha256(
|
|
57
|
+
(
|
|
58
|
+
f"{self.options.entity_name}\x00{self.scanner_name}\x00{self.options.config_id}"
|
|
59
|
+
).encode()
|
|
60
|
+
).hexdigest()[:MACHINE_NAME_DIGEST_LENGTH]
|
|
61
|
+
readable = UNSAFE_NAME_CHARS.sub("_", f"{self.options.entity_name}_{self.scanner_name}")
|
|
62
|
+
budget = MAX_EXECUTION_NAME - MACHINE_NAME_DIGEST_LENGTH - 1
|
|
63
|
+
return f"{readable[:budget]}_{digest}"
|
|
@@ -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
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
ResolvesScanOptions,
|
|
9
|
+
StateMachineComponentOrchestrator,
|
|
10
|
+
)
|
|
11
|
+
from fluidattacks_core.models.state_machines.root_scanner import (
|
|
12
|
+
MACHINE_NAME_DIGEST_LENGTH,
|
|
13
|
+
MAX_EXECUTION_NAME,
|
|
14
|
+
UNSAFE_NAME_CHARS,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _OptionsWithAnExtraField(ComponentScanOptions):
|
|
19
|
+
seed_domain: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _options(**overrides: str) -> ComponentScanOptions:
|
|
23
|
+
arguments: dict[str, str] = {
|
|
24
|
+
"config_id": "123456",
|
|
25
|
+
"entity_name": "acme",
|
|
26
|
+
"user_email": "test@fluidattacks.com",
|
|
27
|
+
}
|
|
28
|
+
arguments.update(overrides)
|
|
29
|
+
return ComponentScanOptions(**arguments)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _wire_payload(**overrides: object) -> dict[str, object]:
|
|
33
|
+
payload: dict[str, object] = {
|
|
34
|
+
"job_definition": "resolves_nix",
|
|
35
|
+
"job_queue": "resolves",
|
|
36
|
+
"scanner_name": "resolves-scan",
|
|
37
|
+
"options": _options(),
|
|
38
|
+
}
|
|
39
|
+
payload.update(overrides)
|
|
40
|
+
return payload
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _machine_input(
|
|
44
|
+
options: ComponentScanOptions | None = None,
|
|
45
|
+
scanner_name: str = "resolves-scan",
|
|
46
|
+
) -> StateMachineComponentOrchestrator[ComponentScanOptions]:
|
|
47
|
+
resolved = options if options is not None else _options()
|
|
48
|
+
return StateMachineComponentOrchestrator[ComponentScanOptions].model_validate(
|
|
49
|
+
_wire_payload(options=resolved, scanner_name=scanner_name)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_orchestrator_serializes_six_wire_keys_from_the_options() -> None:
|
|
54
|
+
options = _options()
|
|
55
|
+
machine_input = _machine_input(options)
|
|
56
|
+
dumped = machine_input.model_dump(mode="json")
|
|
57
|
+
assert machine_input.to_wire() == dumped
|
|
58
|
+
assert set(dumped) == {
|
|
59
|
+
"configId",
|
|
60
|
+
"entityName",
|
|
61
|
+
"executionOptions",
|
|
62
|
+
"jobDefinition",
|
|
63
|
+
"jobQueue",
|
|
64
|
+
"scannerName",
|
|
65
|
+
}
|
|
66
|
+
blob = json.loads(dumped["executionOptions"])
|
|
67
|
+
assert dumped["configId"] == blob["config_id"]
|
|
68
|
+
assert dumped["entityName"] == blob["entity_name"]
|
|
69
|
+
assert ComponentScanOptions.model_validate_json(machine_input.execution_options) == options
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_entity_name_is_a_storage_key_not_a_free_string() -> None:
|
|
73
|
+
for value in ("-h", "--storage-dir", ".", "..", "a..b", "_internal", "a/b", ""):
|
|
74
|
+
with pytest.raises(ValidationError):
|
|
75
|
+
_options(entity_name=value)
|
|
76
|
+
for value in ("acme\n", "acme\t", "acme corp", "gr\u043eup"):
|
|
77
|
+
with pytest.raises(ValidationError):
|
|
78
|
+
_options(entity_name=value)
|
|
79
|
+
for value in ("acme", "acme_nickname", "group.repo-name_v2", "0abc"):
|
|
80
|
+
assert _options(entity_name=value).entity_name == value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_options_refuse_unknown_fields_rebinding_and_unvalidated_instances() -> None:
|
|
84
|
+
with pytest.raises(ValidationError):
|
|
85
|
+
ComponentScanOptions.model_validate({**_options().model_dump(), "cwd": "../../.."})
|
|
86
|
+
options = _options()
|
|
87
|
+
for field in ("config_id", "entity_name", "user_email"):
|
|
88
|
+
with pytest.raises(ValidationError, match="frozen"):
|
|
89
|
+
setattr(options, field, "other")
|
|
90
|
+
smuggled = ComponentScanOptions.model_construct(
|
|
91
|
+
config_id="1", entity_name="../../etc", user_email="e@f"
|
|
92
|
+
)
|
|
93
|
+
with pytest.raises(ValidationError):
|
|
94
|
+
StateMachineComponentOrchestrator[ComponentScanOptions].model_validate(
|
|
95
|
+
_wire_payload(options=smuggled)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_orchestrator_rejects_unknown_and_empty_wire_fields() -> None:
|
|
100
|
+
for overrides in ({"unknown_field": "bad"}, {"job_definition": ""}, {"job_queue": ""}):
|
|
101
|
+
with pytest.raises(ValidationError):
|
|
102
|
+
StateMachineComponentOrchestrator[ComponentScanOptions].model_validate(
|
|
103
|
+
_wire_payload(**overrides)
|
|
104
|
+
)
|
|
105
|
+
for name in ("has space", "has.dot", "-h", "_leading", "sla/sh", ""):
|
|
106
|
+
with pytest.raises(ValidationError):
|
|
107
|
+
_machine_input(scanner_name=name)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_machine_name_stays_valid_at_the_execution_name_limit() -> None:
|
|
111
|
+
longest = _machine_input(
|
|
112
|
+
options=_options(config_id="c" * 128, entity_name="a" * 128),
|
|
113
|
+
scanner_name="s" * 200,
|
|
114
|
+
)
|
|
115
|
+
garbage = ComponentScanOptions.model_construct(
|
|
116
|
+
config_id="c" * 200, entity_name="a corp/../etc", user_email="e@f"
|
|
117
|
+
)
|
|
118
|
+
unvalidated = StateMachineComponentOrchestrator[ComponentScanOptions].model_construct(
|
|
119
|
+
job_definition="resolves_nix",
|
|
120
|
+
job_queue="resolves",
|
|
121
|
+
scanner_name="resolves scan/../x",
|
|
122
|
+
options=garbage,
|
|
123
|
+
)
|
|
124
|
+
for machine_input in (longest, unvalidated):
|
|
125
|
+
name = machine_input.get_machine_name()
|
|
126
|
+
assert len(name) <= MAX_EXECUTION_NAME
|
|
127
|
+
assert not UNSAFE_NAME_CHARS.search(name)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_machine_name_stays_greppable_by_entity_and_scanner() -> None:
|
|
131
|
+
assert _machine_input().get_machine_name().startswith("acme_resolves-scan_")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_machine_name_is_injective() -> None:
|
|
135
|
+
baseline = _machine_input().get_machine_name()
|
|
136
|
+
assert baseline != _machine_input(scanner_name="resolves-dast-scan").get_machine_name()
|
|
137
|
+
for field in ("config_id", "entity_name"):
|
|
138
|
+
assert baseline != _machine_input(_options(**{field: "other"})).get_machine_name()
|
|
139
|
+
hidden = "e" * (MAX_EXECUTION_NAME - MACHINE_NAME_DIGEST_LENGTH)
|
|
140
|
+
entities = {
|
|
141
|
+
_machine_input(_options(entity_name=f"{hidden}{suffix}")).get_machine_name()
|
|
142
|
+
for suffix in ("a", "b")
|
|
143
|
+
}
|
|
144
|
+
masked = {
|
|
145
|
+
_machine_input(_options(entity_name=hidden), scanner).get_machine_name()
|
|
146
|
+
for scanner in ("resolves-scan", "resolves-dast-scan")
|
|
147
|
+
}
|
|
148
|
+
filled = "a" * (MAX_EXECUTION_NAME - MACHINE_NAME_DIGEST_LENGTH - 1)
|
|
149
|
+
fenced = {
|
|
150
|
+
_machine_input(_options(entity_name=f"{filled}{tail}"), scanner).get_machine_name()
|
|
151
|
+
for tail, scanner in (("bc", "d"), ("b", "cd"))
|
|
152
|
+
}
|
|
153
|
+
assert len(entities) == 2
|
|
154
|
+
assert len(masked) == 2
|
|
155
|
+
assert len(fenced) == 2
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_orchestrator_binds_the_options_type_it_is_parameterized_with() -> None:
|
|
159
|
+
extended = _OptionsWithAnExtraField(
|
|
160
|
+
config_id="123456",
|
|
161
|
+
entity_name="acme",
|
|
162
|
+
seed_domain="acme.com",
|
|
163
|
+
user_email="test@fluidattacks.com",
|
|
164
|
+
)
|
|
165
|
+
machine_input = StateMachineComponentOrchestrator[_OptionsWithAnExtraField].model_validate(
|
|
166
|
+
_wire_payload(options=extended)
|
|
167
|
+
)
|
|
168
|
+
assert json.loads(machine_input.execution_options)["seed_domain"] == "acme.com"
|
|
169
|
+
with pytest.raises(ValidationError):
|
|
170
|
+
StateMachineComponentOrchestrator[_OptionsWithAnExtraField].model_validate(_wire_payload())
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_resolves_options_add_no_fields_of_their_own() -> None:
|
|
174
|
+
options = ResolvesScanOptions(
|
|
175
|
+
config_id="123456", entity_name="acme", user_email="test@fluidattacks.com"
|
|
176
|
+
)
|
|
177
|
+
with pytest.raises(ValidationError):
|
|
178
|
+
ResolvesScanOptions.model_validate({**options.model_dump(), "seed_domain": "acme.com"})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/tests/test_loader_env.py
RENAMED
|
File without changes
|
{fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/tests/test_model_scanner.py
RENAMED
|
File without changes
|
{fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/tests/test_root_scanner.py
RENAMED
|
File without changes
|
{fluidattacks_core_models-13.0.0 → fluidattacks_core_models-13.2.0}/tests/test_state_machines.py
RENAMED
|
File without changes
|