forked from ChelseaKR/oscal-validate
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_break_the_gate.py
More file actions
158 lines (115 loc) · 6.62 KB
/
Copy pathtest_break_the_gate.py
File metadata and controls
158 lines (115 loc) · 6.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""Break the gate on purpose before trusting it.
Discipline these tests encode: a gate is only trusted after deliberately
corrupting a known-good document and confirming the gate catches the
corruption. Each test starts from a fixture proven clean first, breaks exactly
one thing, and asserts the specific catch.
"""
from __future__ import annotations
import copy
from pathlib import Path
from typing import Any
import pytest
from oscal_validate import Severity, validate_file
from .conftest import fixture_path, load_fixture, write
def _codes(path: Path, resolve: list[Path] | None = None) -> set[str]:
return {f.code for f in validate_file(path, resolve)}
def _errors(path: Path, resolve: list[Path] | None = None) -> set[str]:
return {f.code for f in validate_file(path, resolve) if f.severity is Severity.ERROR}
@pytest.fixture
def clean_catalog() -> Any:
findings = validate_file(fixture_path("clean_catalog.json"))
assert not [f for f in findings if f.severity is Severity.ERROR], (
"gate tests require a proven-clean baseline"
)
return copy.deepcopy(load_fixture("clean_catalog.json"))
def test_removing_a_required_property_is_caught(tmp_path: Path, clean_catalog: Any) -> None:
del clean_catalog["catalog"]["metadata"]["last-modified"]
assert "REQUIRED_PROPERTY_MISSING" in _errors(write(tmp_path, "c.json", clean_catalog))
def test_a_property_the_schema_forbids_is_caught(tmp_path: Path, clean_catalog: Any) -> None:
clean_catalog["catalog"]["metadata"]["invented-property"] = "hello"
assert "PROPERTY_UNDECLARED" in _errors(write(tmp_path, "c.json", clean_catalog))
def test_a_malformed_uuid_is_caught(tmp_path: Path, clean_catalog: Any) -> None:
clean_catalog["catalog"]["uuid"] = "not-a-uuid"
assert "DATATYPE_MISMATCH" in _errors(write(tmp_path, "c.json", clean_catalog))
def test_a_uuid_v1_is_caught_because_oscal_requires_v4_or_v5(
tmp_path: Path, clean_catalog: Any
) -> None:
# OSCAL's UUIDDatatype pattern pins the version nibble to 4 or 5.
clean_catalog["catalog"]["uuid"] = "f0d0a6cd-9e0e-1c2b-9b3e-0a3f2f7a1c11"
assert "DATATYPE_MISMATCH" in _errors(write(tmp_path, "c.json", clean_catalog))
def test_a_timestamp_without_a_timezone_is_caught(tmp_path: Path, clean_catalog: Any) -> None:
clean_catalog["catalog"]["metadata"]["last-modified"] = "2026-08-14T00:00:00"
assert "DATATYPE_MISMATCH" in _errors(write(tmp_path, "c.json", clean_catalog))
def test_a_wrong_json_type_is_caught(tmp_path: Path, clean_catalog: Any) -> None:
clean_catalog["catalog"]["metadata"]["title"] = {"not": "a string"}
assert "TYPE_MISMATCH" in _errors(write(tmp_path, "c.json", clean_catalog))
def test_a_duplicate_uuid_is_caught(tmp_path: Path, clean_catalog: Any) -> None:
clean_catalog["catalog"]["metadata"]["parties"][0]["uuid"] = clean_catalog["catalog"]["uuid"]
assert "UUID_NOT_UNIQUE" in _errors(write(tmp_path, "c.json", clean_catalog))
def test_a_duplicate_control_id_is_caught(tmp_path: Path, clean_catalog: Any) -> None:
controls = clean_catalog["catalog"]["groups"][0]["controls"]
controls[1]["id"] = controls[0]["id"]
assert "CONSTRAINT_NOT_UNIQUE" in _errors(write(tmp_path, "c.json", clean_catalog))
def test_a_role_id_that_names_no_role_is_caught(tmp_path: Path, clean_catalog: Any) -> None:
clean_catalog["catalog"]["metadata"]["responsible-parties"][0]["role-id"] = "no-such-role"
assert "REFERENCE_UNRESOLVED" in _errors(write(tmp_path, "c.json", clean_catalog))
def test_a_party_uuid_that_names_no_party_is_caught(tmp_path: Path, clean_catalog: Any) -> None:
clean_catalog["catalog"]["metadata"]["responsible-parties"][0]["party-uuids"] = [
"11111111-2222-4333-8444-555555555555"
]
assert "REFERENCE_UNRESOLVED" in _errors(write(tmp_path, "c.json", clean_catalog))
def test_a_dangling_back_matter_fragment_is_caught(tmp_path: Path, clean_catalog: Any) -> None:
clean_catalog["catalog"]["metadata"]["links"][0]["href"] = (
"#99999999-8888-4777-8666-555555555555"
)
assert "REFERENCE_UNRESOLVED" in _errors(write(tmp_path, "c.json", clean_catalog))
def test_a_profile_control_reference_that_misses_is_caught_only_with_the_catalog(
tmp_path: Path,
) -> None:
profile = copy.deepcopy(load_fixture("clean_profile.json"))
profile["profile"]["imports"][0]["include-controls"][0]["with-ids"] = ["ex-1", "ex-99"]
path = write(tmp_path, "clean_profile.json", profile)
catalog = fixture_path("clean_catalog.json")
# Without the catalog the answer is unknown, and unknown is not a failure.
assert "REFERENCE_UNRESOLVED" not in _errors(path)
assert "REFERENCE_UNVERIFIABLE" in _codes(path)
# With it, the effective data model is complete and the miss is an error.
assert "REFERENCE_UNRESOLVED" in _errors(path, [catalog])
def test_a_lookup_into_an_index_that_was_never_built_is_never_an_error(
tmp_path: Path, clean_catalog: Any
) -> None:
"""The other direction of the same rule: a skipped constraint accuses nobody.
NIST builds ``index-metadata-party-organizations-uuid`` with an ``index``
constraint whose target is ``party[@type='organization']``, a predicate
outside the Metapath subset this tool parses, so that index is never
populated. The ``index-has-key`` constraint that reads it *is* evaluated. A
lookup into an empty index misses every key, so reporting the miss as a
failure would report a rule this tool did not evaluate as a defect in
someone's document. Here the organization named is the one the fixture
declares, and it must not be an ERROR.
"""
organization = clean_catalog["catalog"]["metadata"]["parties"][0]
clean_catalog["catalog"]["metadata"]["parties"].append(
{
"uuid": "5c2b0d18-2b7a-4f6e-9a0e-2c1d3e4f5a60",
"type": "person",
"name": "Example Person",
"member-of-organizations": [organization["uuid"]],
}
)
path = write(tmp_path, "c.json", clean_catalog)
assert "REFERENCE_UNRESOLVED" not in _errors(path)
unsettled = [
f
for f in validate_file(path)
if f.code == "REFERENCE_UNVERIFIABLE"
and "index-metadata-party-organizations-uuid" in f.message
]
assert unsettled, "the unbuilt index must be named, not silently passed"
assert all(f.severity is Severity.UNVERIFIABLE for f in unsettled)
def test_an_object_no_schema_alternative_accepts_is_caught(
tmp_path: Path, clean_catalog: Any
) -> None:
# A group may hold controls or groups, never both.
clean_catalog["catalog"]["groups"][0]["groups"] = [{"id": "sub", "title": "Sub"}]
assert "NO_SCHEMA_ALTERNATIVE" in _errors(write(tmp_path, "c.json", clean_catalog))