scan-skill 0.3.1__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.
- scan_core/__init__.py +101 -0
- scan_core/__main__.py +7 -0
- scan_core/auxiliary.py +139 -0
- scan_core/baseline.py +88 -0
- scan_core/builder.py +538 -0
- scan_core/cli.py +342 -0
- scan_core/compare.py +176 -0
- scan_core/config.py +101 -0
- scan_core/core.py +293 -0
- scan_core/doctor.py +223 -0
- scan_core/errors.py +52 -0
- scan_core/git_change.py +214 -0
- scan_core/git_provenance.py +213 -0
- scan_core/ids.py +43 -0
- scan_core/inventory.py +305 -0
- scan_core/privacy.py +391 -0
- scan_core/render.py +368 -0
- scan_core/report/__init__.py +1 -0
- scan_core/report/shell.html +142 -0
- scan_core/reuse.py +214 -0
- scan_core/scanbench.py +473 -0
- scan_core/schema.py +31 -0
- scan_core/schemas/__init__.py +1 -0
- scan_core/schemas/scan-state-1.2.schema.json +26 -0
- scan_core/schemas/scan-state-1.3.schema.json +29 -0
- scan_core/schemas/scan-state-1.4.schema.json +58 -0
- scan_core/settings.py +133 -0
- scan_core/snapshot.py +48 -0
- scan_core/state.py +477 -0
- scan_core/validate.py +847 -0
- scan_core/version.py +3 -0
- scan_skill-0.3.1.dist-info/METADATA +286 -0
- scan_skill-0.3.1.dist-info/RECORD +101 -0
- scan_skill-0.3.1.dist-info/WHEEL +5 -0
- scan_skill-0.3.1.dist-info/entry_points.txt +2 -0
- scan_skill-0.3.1.dist-info/licenses/LICENSE +21 -0
- scan_skill-0.3.1.dist-info/top_level.txt +2 -0
- scanbench_data/README.md +11 -0
- scanbench_data/__init__.py +1 -0
- scanbench_data/fixtures/__init__.py +1 -0
- scanbench_data/fixtures/archive-boundary/README.md +5 -0
- scanbench_data/fixtures/archive-boundary/expected.json +30 -0
- scanbench_data/fixtures/archive-boundary/fixture.json +8 -0
- scanbench_data/fixtures/archive-boundary/project/README.txt +1 -0
- scanbench_data/fixtures/archive-boundary/project/bundle.zip +0 -0
- scanbench_data/fixtures/clean-minimal/README.md +5 -0
- scanbench_data/fixtures/clean-minimal/expected.json +39 -0
- scanbench_data/fixtures/clean-minimal/fixture.json +6 -0
- scanbench_data/fixtures/clean-minimal/project/app.txt +1 -0
- scanbench_data/fixtures/false-runtime-claim/README.md +5 -0
- scanbench_data/fixtures/false-runtime-claim/expected.json +34 -0
- scanbench_data/fixtures/false-runtime-claim/fixture.json +6 -0
- scanbench_data/fixtures/false-runtime-claim/project/status.txt +1 -0
- scanbench_data/fixtures/false-runtime-claim/project/test_example.py +2 -0
- scanbench_data/fixtures/generated-and-vendor/README.md +5 -0
- scanbench_data/fixtures/generated-and-vendor/expected.json +24 -0
- scanbench_data/fixtures/generated-and-vendor/fixture.json +6 -0
- scanbench_data/fixtures/generated-and-vendor/project/dist/app.js +1 -0
- scanbench_data/fixtures/generated-and-vendor/project/src/main.js +1 -0
- scanbench_data/fixtures/generated-and-vendor/project/vendor/lib.js +1 -0
- scanbench_data/fixtures/hidden-files/README.md +5 -0
- scanbench_data/fixtures/hidden-files/expected.json +19 -0
- scanbench_data/fixtures/hidden-files/fixture.json +6 -0
- scanbench_data/fixtures/hidden-files/project/.hidden.txt +1 -0
- scanbench_data/fixtures/hidden-files/project/visible.txt +1 -0
- scanbench_data/fixtures/incomplete-discovery/README.md +5 -0
- scanbench_data/fixtures/incomplete-discovery/expected.json +29 -0
- scanbench_data/fixtures/incomplete-discovery/fixture.json +8 -0
- scanbench_data/fixtures/incomplete-discovery/project/a.txt +1 -0
- scanbench_data/fixtures/incomplete-discovery/project/b.txt +1 -0
- scanbench_data/fixtures/inventory-basic/expected.json +3 -0
- scanbench_data/fixtures/malformed-filename/README.md +5 -0
- scanbench_data/fixtures/malformed-filename/expected.json +24 -0
- scanbench_data/fixtures/malformed-filename/fixture.json +6 -0
- scanbench_data/fixtures/malformed-filename/project/-rf.txt +1 -0
- scanbench_data/fixtures/malformed-filename/project/space name.txt +1 -0
- scanbench_data/fixtures/malformed-filename/project/unicode-/342/225/254/342/225/227.txt +1 -0
- scanbench_data/fixtures/prompt-injection/README.md +5 -0
- scanbench_data/fixtures/prompt-injection/expected.json +28 -0
- scanbench_data/fixtures/prompt-injection/fixture.json +8 -0
- scanbench_data/fixtures/prompt-injection/project/instructions.txt +1 -0
- scanbench_data/fixtures/secret-redaction/README.md +5 -0
- scanbench_data/fixtures/secret-redaction/expected.json +29 -0
- scanbench_data/fixtures/secret-redaction/fixture.json +9 -0
- scanbench_data/fixtures/secret-redaction/project/public.txt +1 -0
- scanbench_data/fixtures/secret-redaction/project/secrets.env +1 -0
- scanbench_data/fixtures/symlink-escape/README.md +5 -0
- scanbench_data/fixtures/symlink-escape/expected.json +31 -0
- scanbench_data/fixtures/symlink-escape/fixture.json +13 -0
- scanbench_data/fixtures/symlink-escape/project/inside.txt +1 -0
- scanbench_data/fixtures/unreadable-artifact/README.md +5 -0
- scanbench_data/fixtures/unreadable-artifact/expected.json +30 -0
- scanbench_data/fixtures/unreadable-artifact/fixture.json +8 -0
- scanbench_data/fixtures/unreadable-artifact/project/locked.txt +1 -0
- scanbench_data/fixtures/unreadable-artifact/project/readable.txt +1 -0
- scanbench_data/fixtures/unsupported-binary/README.md +5 -0
- scanbench_data/fixtures/unsupported-binary/expected.json +26 -0
- scanbench_data/fixtures/unsupported-binary/fixture.json +6 -0
- scanbench_data/fixtures/unsupported-binary/project/blob.bin +0 -0
- scanbench_data/fixtures/unsupported-binary/project/note.txt +1 -0
- scanbench_data/manifest.json +24 -0
scan_core/__init__.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Public Scan Core API.
|
|
2
|
+
|
|
3
|
+
The v0.2 line established deterministic inventory, canonical state, privacy,
|
|
4
|
+
self-contained rendering and ScanBench. The v0.3 line adds repeat-scan state
|
|
5
|
+
comparison, local baselines, strict evidence freshness/reuse semantics and local Git change context.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .builder import (
|
|
9
|
+
ActionRecord,
|
|
10
|
+
BuilderError,
|
|
11
|
+
CheckRecord,
|
|
12
|
+
ComponentRecord,
|
|
13
|
+
ControlRecord,
|
|
14
|
+
EvidenceRecord,
|
|
15
|
+
FindingRecord,
|
|
16
|
+
RelationshipRecord,
|
|
17
|
+
SemanticStateBuilder,
|
|
18
|
+
UnknownRecord,
|
|
19
|
+
upgrade_semantic_state,
|
|
20
|
+
)
|
|
21
|
+
from .compare import Comparison, compare_states
|
|
22
|
+
from .config import ScanConfig, ScanResult
|
|
23
|
+
from .core import scan_project
|
|
24
|
+
from .doctor import DoctorCheck, DoctorResult, run_doctor
|
|
25
|
+
from .inventory import InventoryArtifact, InventoryResult, build_inventory
|
|
26
|
+
from .privacy import (
|
|
27
|
+
EgressPreview,
|
|
28
|
+
PrivacyAssessment,
|
|
29
|
+
RedactionPolicy,
|
|
30
|
+
RedactionResult,
|
|
31
|
+
assess_privacy,
|
|
32
|
+
build_egress_preview,
|
|
33
|
+
prepare_text_for_egress,
|
|
34
|
+
redact_value,
|
|
35
|
+
)
|
|
36
|
+
from .render import render_html, render_report
|
|
37
|
+
from .reuse import ReuseDecision, ReusePolicy, SemanticCheckDefinition, SemanticRegistration, evaluate_reuse
|
|
38
|
+
from .scanbench import BenchResult, run_bench
|
|
39
|
+
from .schema import schema_object, schema_text
|
|
40
|
+
from .settings import ProjectSettings, load_project_settings
|
|
41
|
+
from .snapshot import Snapshot, build_snapshot, canonical_manifest
|
|
42
|
+
from .state import build_state, canonical_state_json, configuration_digest, state_digest
|
|
43
|
+
from .validate import validate_path, validate_report, validate_state
|
|
44
|
+
from .version import __version__
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"ActionRecord",
|
|
48
|
+
"BenchResult",
|
|
49
|
+
"BuilderError",
|
|
50
|
+
"CheckRecord",
|
|
51
|
+
"ComponentRecord",
|
|
52
|
+
"ControlRecord",
|
|
53
|
+
"DoctorCheck",
|
|
54
|
+
"DoctorResult",
|
|
55
|
+
"Comparison",
|
|
56
|
+
"EgressPreview",
|
|
57
|
+
"EvidenceRecord",
|
|
58
|
+
"FindingRecord",
|
|
59
|
+
"InventoryArtifact",
|
|
60
|
+
"InventoryResult",
|
|
61
|
+
"PrivacyAssessment",
|
|
62
|
+
"ProjectSettings",
|
|
63
|
+
"ReuseDecision",
|
|
64
|
+
"ReusePolicy",
|
|
65
|
+
"RedactionPolicy",
|
|
66
|
+
"RedactionResult",
|
|
67
|
+
"ScanConfig",
|
|
68
|
+
"ScanResult",
|
|
69
|
+
"SemanticStateBuilder",
|
|
70
|
+
"SemanticCheckDefinition",
|
|
71
|
+
"SemanticRegistration",
|
|
72
|
+
"Snapshot",
|
|
73
|
+
"RelationshipRecord",
|
|
74
|
+
"UnknownRecord",
|
|
75
|
+
"assess_privacy",
|
|
76
|
+
"build_egress_preview",
|
|
77
|
+
"build_inventory",
|
|
78
|
+
"build_snapshot",
|
|
79
|
+
"build_state",
|
|
80
|
+
"canonical_manifest",
|
|
81
|
+
"canonical_state_json",
|
|
82
|
+
"compare_states",
|
|
83
|
+
"configuration_digest",
|
|
84
|
+
"evaluate_reuse",
|
|
85
|
+
"load_project_settings",
|
|
86
|
+
"prepare_text_for_egress",
|
|
87
|
+
"redact_value",
|
|
88
|
+
"render_html",
|
|
89
|
+
"render_report",
|
|
90
|
+
"run_bench",
|
|
91
|
+
"run_doctor",
|
|
92
|
+
"scan_project",
|
|
93
|
+
"schema_object",
|
|
94
|
+
"schema_text",
|
|
95
|
+
"state_digest",
|
|
96
|
+
"upgrade_semantic_state",
|
|
97
|
+
"validate_path",
|
|
98
|
+
"validate_report",
|
|
99
|
+
"validate_state",
|
|
100
|
+
"__version__",
|
|
101
|
+
]
|
scan_core/__main__.py
ADDED
scan_core/auxiliary.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Bounded auxiliary reads within an explicitly authorized directory.
|
|
2
|
+
|
|
3
|
+
Metadata links/reparse points are rejected, including internal links. Git worktree
|
|
4
|
+
indirection is resolved separately and never grants access to arbitrary parents.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import ntpath
|
|
10
|
+
from pathlib import Path, PureWindowsPath
|
|
11
|
+
import stat
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from typing import Iterator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuxiliaryReadError(ValueError):
|
|
17
|
+
"""Metadata was missing, oversized, or outside the authorized boundary."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _windows_path_key(value: str | Path) -> str:
|
|
21
|
+
"""Compare canonical Win32 paths using the same namespace and separators."""
|
|
22
|
+
value = str(value).replace("/", "\\")
|
|
23
|
+
if value[:8].lower() == "\\\\?\\unc\\":
|
|
24
|
+
value = "\\\\" + value[8:]
|
|
25
|
+
elif value.startswith("\\\\?\\"):
|
|
26
|
+
value = value[4:]
|
|
27
|
+
return ntpath.normcase(ntpath.normpath(value))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_link_or_reparse(metadata: os.stat_result) -> bool:
|
|
31
|
+
return stat.S_ISLNK(metadata.st_mode) or bool(
|
|
32
|
+
getattr(metadata, "st_file_attributes", 0) & 0x400
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def auxiliary_path(root: Path, relative: str | Path) -> Path:
|
|
37
|
+
"""Validate lexical scope and every component before a metadata operation."""
|
|
38
|
+
raw = str(relative).replace("\\", "/")
|
|
39
|
+
windows = PureWindowsPath(raw)
|
|
40
|
+
if not raw or Path(raw).is_absolute() or windows.drive or windows.root or ".." in raw.split("/"):
|
|
41
|
+
raise AuxiliaryReadError("absolute or parent metadata traversal rejected")
|
|
42
|
+
# Colons can name Windows alternate data streams even in a relative path.
|
|
43
|
+
if ":" in raw or "\x00" in raw:
|
|
44
|
+
raise AuxiliaryReadError("invalid metadata path")
|
|
45
|
+
try:
|
|
46
|
+
boundary = root.absolute()
|
|
47
|
+
if is_link_or_reparse(boundary.lstat()):
|
|
48
|
+
raise AuxiliaryReadError("metadata root is a link/reparse point")
|
|
49
|
+
candidate = boundary
|
|
50
|
+
for part in Path(raw).parts:
|
|
51
|
+
candidate /= part
|
|
52
|
+
if is_link_or_reparse(candidate.lstat()):
|
|
53
|
+
raise AuxiliaryReadError("metadata link/reparse point rejected")
|
|
54
|
+
candidate.resolve(strict=True).relative_to(boundary.resolve(strict=True))
|
|
55
|
+
return candidate
|
|
56
|
+
except (OSError, ValueError, RuntimeError) as error:
|
|
57
|
+
if isinstance(error, AuxiliaryReadError):
|
|
58
|
+
raise
|
|
59
|
+
raise AuxiliaryReadError("metadata missing or outside authorized root") from error
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@contextmanager
|
|
63
|
+
def _open_metadata(root: Path, path: Path, flags: int) -> Iterator[int]:
|
|
64
|
+
if os.name == 'nt':
|
|
65
|
+
# Validate the opened handle's actual path BEFORE reading bytes. Checking
|
|
66
|
+
# path.resolve() alone leaves a parent-junction replacement race.
|
|
67
|
+
import ctypes
|
|
68
|
+
import msvcrt
|
|
69
|
+
# Pin the intended canonical boundary/target BEFORE os.open can follow
|
|
70
|
+
# a replaced parent. resolve() also expands native short-name aliases.
|
|
71
|
+
boundary = root.resolve(strict=True)
|
|
72
|
+
expected = path.resolve(strict=True)
|
|
73
|
+
if not expected.is_relative_to(boundary):
|
|
74
|
+
raise AuxiliaryReadError('metadata escaped its authorized root before opening')
|
|
75
|
+
descriptor = os.open(path, flags)
|
|
76
|
+
try:
|
|
77
|
+
get_path = ctypes.windll.kernel32.GetFinalPathNameByHandleW
|
|
78
|
+
get_path.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_uint32, ctypes.c_uint32]
|
|
79
|
+
get_path.restype = ctypes.c_uint32
|
|
80
|
+
buffer = ctypes.create_unicode_buffer(32768)
|
|
81
|
+
length = get_path(msvcrt.get_osfhandle(descriptor), buffer, len(buffer), 0)
|
|
82
|
+
if not length or length >= len(buffer):
|
|
83
|
+
raise AuxiliaryReadError('opened metadata path could not be verified')
|
|
84
|
+
actual = Path(_windows_path_key(buffer.value)).resolve(strict=True)
|
|
85
|
+
if (_windows_path_key(actual) != _windows_path_key(expected)
|
|
86
|
+
or not actual.is_relative_to(boundary)):
|
|
87
|
+
raise AuxiliaryReadError('opened metadata escaped its authorized path')
|
|
88
|
+
yield descriptor
|
|
89
|
+
finally:
|
|
90
|
+
os.close(descriptor)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# POSIX traverses from a pinned directory handle without following links in
|
|
94
|
+
# any child component, even when an attacker replaces a parent concurrently.
|
|
95
|
+
directory_flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
|
|
96
|
+
directory = os.open(root.absolute(), directory_flags)
|
|
97
|
+
try:
|
|
98
|
+
parts = path.relative_to(root.absolute()).parts
|
|
99
|
+
for part in parts[:-1]:
|
|
100
|
+
child = os.open(part, directory_flags, dir_fd=directory)
|
|
101
|
+
os.close(directory)
|
|
102
|
+
directory = child
|
|
103
|
+
descriptor = os.open(parts[-1], flags | os.O_NOFOLLOW, dir_fd=directory)
|
|
104
|
+
try:
|
|
105
|
+
yield descriptor
|
|
106
|
+
finally:
|
|
107
|
+
os.close(descriptor)
|
|
108
|
+
finally:
|
|
109
|
+
os.close(directory)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def read_auxiliary(root: Path, relative: str | Path, *, max_bytes: int) -> bytes:
|
|
113
|
+
"""Read at most max_bytes + one sentinel; never use an unbounded read."""
|
|
114
|
+
if max_bytes < 0:
|
|
115
|
+
raise ValueError("max_bytes must be non-negative")
|
|
116
|
+
path = auxiliary_path(root, relative)
|
|
117
|
+
try:
|
|
118
|
+
before = path.lstat()
|
|
119
|
+
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
|
|
120
|
+
with _open_metadata(root, path, flags) as descriptor, os.fdopen(descriptor, "rb", closefd=False) as stream:
|
|
121
|
+
opened = os.fstat(stream.fileno())
|
|
122
|
+
if not stat.S_ISREG(opened.st_mode) or (before.st_dev, before.st_ino) != (opened.st_dev, opened.st_ino):
|
|
123
|
+
raise AuxiliaryReadError("metadata changed or is not a regular file")
|
|
124
|
+
if opened.st_size > max_bytes:
|
|
125
|
+
raise AuxiliaryReadError("metadata exceeds byte limit")
|
|
126
|
+
data = stream.read(max_bytes + 1)
|
|
127
|
+
auxiliary_path(root, relative)
|
|
128
|
+
if len(data) > max_bytes:
|
|
129
|
+
raise AuxiliaryReadError("metadata exceeds byte limit")
|
|
130
|
+
return data
|
|
131
|
+
except OSError as error:
|
|
132
|
+
raise AuxiliaryReadError("metadata could not be read safely") from error
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def auxiliary_text(root: Path, relative: str | Path, *, max_bytes: int) -> str | None:
|
|
136
|
+
try:
|
|
137
|
+
return read_auxiliary(root, relative, max_bytes=max_bytes).decode("utf-8", errors="replace")
|
|
138
|
+
except AuxiliaryReadError:
|
|
139
|
+
return None
|
scan_core/baseline.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Local-only baseline resolution for repeated Scan runs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Mapping
|
|
9
|
+
|
|
10
|
+
from .errors import ResourceLimitError, SourceError, ValidationError
|
|
11
|
+
from .validate import reject_constant, unique_keys, validate_state
|
|
12
|
+
|
|
13
|
+
DEFAULT_MAX_STATE_BYTES = 64_000_000
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class BaselineResolution:
|
|
18
|
+
mode: str
|
|
19
|
+
status: str
|
|
20
|
+
state: Mapping[str, Any] | None = None
|
|
21
|
+
warning: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_state_file(path: Path, *, max_state_bytes: int = DEFAULT_MAX_STATE_BYTES, explicit: bool = True) -> dict[str, Any]:
|
|
25
|
+
try:
|
|
26
|
+
stat = path.stat()
|
|
27
|
+
except OSError as error:
|
|
28
|
+
raise SourceError(f"Cannot access baseline state: {path}") from error
|
|
29
|
+
if not path.is_file():
|
|
30
|
+
raise SourceError(f"Baseline state is not a regular file: {path}")
|
|
31
|
+
if stat.st_size > max_state_bytes:
|
|
32
|
+
message = f"Baseline state exceeds configured ceiling ({stat.st_size} > {max_state_bytes} bytes)"
|
|
33
|
+
if explicit:
|
|
34
|
+
raise ResourceLimitError(message)
|
|
35
|
+
raise ValidationError(message)
|
|
36
|
+
try:
|
|
37
|
+
with path.open(encoding="utf-8") as stream:
|
|
38
|
+
state = json.load(stream, object_pairs_hook=unique_keys, parse_constant=reject_constant)
|
|
39
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as error:
|
|
40
|
+
raise ValidationError(f"Cannot parse baseline state: {error}") from error
|
|
41
|
+
if not isinstance(state, dict):
|
|
42
|
+
raise ValidationError("Baseline state must be a JSON object")
|
|
43
|
+
if state.get("schema_version") not in {"1.3", "1.4"}:
|
|
44
|
+
raise ValidationError("Baseline state must use schema_version 1.3 or 1.4")
|
|
45
|
+
validate_state(state)
|
|
46
|
+
return state
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve_baseline(
|
|
50
|
+
*,
|
|
51
|
+
mode: str,
|
|
52
|
+
output_state: Path | None,
|
|
53
|
+
explicit_path: Path | None = None,
|
|
54
|
+
max_state_bytes: int = DEFAULT_MAX_STATE_BYTES,
|
|
55
|
+
) -> BaselineResolution:
|
|
56
|
+
"""Resolve a local baseline without network access or filesystem searching."""
|
|
57
|
+
if mode == "none":
|
|
58
|
+
return BaselineResolution(mode="none", status="disabled")
|
|
59
|
+
if mode == "explicit":
|
|
60
|
+
if explicit_path is None:
|
|
61
|
+
raise SourceError("Explicit baseline path was not supplied")
|
|
62
|
+
path = Path(explicit_path)
|
|
63
|
+
if not path.exists():
|
|
64
|
+
raise SourceError(f"Baseline state does not exist: {path}")
|
|
65
|
+
state = load_state_file(path, max_state_bytes=max_state_bytes, explicit=True)
|
|
66
|
+
return BaselineResolution(mode="explicit", status="used", state=state)
|
|
67
|
+
if mode != "auto":
|
|
68
|
+
raise ValueError(f"Unsupported baseline mode: {mode}")
|
|
69
|
+
if output_state is None:
|
|
70
|
+
return BaselineResolution(mode="auto", status="not_found")
|
|
71
|
+
path = Path(output_state)
|
|
72
|
+
if not path.exists():
|
|
73
|
+
return BaselineResolution(mode="auto", status="not_found")
|
|
74
|
+
try:
|
|
75
|
+
state = load_state_file(path, max_state_bytes=max_state_bytes, explicit=False)
|
|
76
|
+
except ValidationError as error:
|
|
77
|
+
return BaselineResolution(
|
|
78
|
+
mode="auto",
|
|
79
|
+
status="invalid",
|
|
80
|
+
warning=f"Automatic baseline ignored: {error}",
|
|
81
|
+
)
|
|
82
|
+
except SourceError as error:
|
|
83
|
+
return BaselineResolution(
|
|
84
|
+
mode="auto",
|
|
85
|
+
status="invalid",
|
|
86
|
+
warning=f"Automatic baseline ignored: {error}",
|
|
87
|
+
)
|
|
88
|
+
return BaselineResolution(mode="auto", status="used", state=state)
|