Skip to content

feat(schema): Finding with §3.1-§3.6 validators (multi-artifact, caveats, MITRE) [W1.B.5]#10

Draft
TimothyVang wants to merge 1 commit into
feat/W1.B.1-artifactclass-enumfrom
feat/W1.B.5-finding-schema
Draft

feat(schema): Finding with §3.1-§3.6 validators (multi-artifact, caveats, MITRE) [W1.B.5]#10
TimothyVang wants to merge 1 commit into
feat/W1.B.1-artifactclass-enumfrom
feat/W1.B.5-finding-schema

Conversation

@TimothyVang

Copy link
Copy Markdown
Owner

Summary

  • Implements verdict/schemas/finding.py — the schema a SANS judge will scrutinise. Encodes CLAUDE.md §3.1–§3.6 invariants at the Pydantic v2 layer.
  • Adds verdict/schemas/caveat_id.py with the seven Tier-1 CaveatID enum members (§3.3).
  • Adds tests/schemas/test_finding.py — 54 tests driving every validator through real Pydantic instantiation (no mocks per §3.10).

Stacked on top of feat/W1.B.1-artifactclass-enum so the ArtifactClass enum is in scope; rebase to main once W1.B.1 lands.

§3 contract enforcement

Rule Field / validator Test
§3.2 multi-artifact artifact_paths: list[str] = Field(min_length=2) test_artifact_paths_min_length_2_*
§3.2 multi-artifact artifact_classes: list[ArtifactClass] = Field(min_length=2) test_artifact_classes_min_length_2_*
§3.2 execution-class _execution_requires_two_classes — T1059/T1106/T1204/T1218/T1543/T1547 (parents + sub-techniques) require ≥2 distinct ArtifactClass values test_execution_claim_with_duplicate_class_is_rejected[*] (boundary)
§3.3 caveats _caveats_acknowledged_required — AMCACHE / SHIMCACHE / PREFETCH / MFT triggers test_amcache_citation_without_caveat_is_rejected, test_mft_citation_with_either_caveat_is_accepted[*]
§3.5 MITRE mitre_technique: str = Field(pattern=^T\d{4}(\.\d{3})?$) test_mitre_regex_accepts_valid_shapes[*], test_mitre_regex_rejects_malformed[*]
§3.6 statuses VerdictStatus enum — exactly six canonical values test_verdict_status_enum_has_exactly_six_canonical_values
§3.6 review_state Literal[\"DRAFT\",\"APPROVED\",\"REJECTED\"] orthogonal to status test_review_state_*, test_review_state_is_separate_field_from_status

Caveats deliberately out-of-scope

SYSMON_PROCESSGUID_OVER_PID and LOGON_TYPE_3_VS_10 are not pure artifact_classes-membership triggers per CLAUDE.md §3.3 and are explicitly enforced upstream (executor wrapper / correlator). The Finding schema covers the four caveats whose triggers are unambiguously class-membership (AMCACHE, SHIMCACHE, PREFETCH, MFT). MFT accepts either MFT_SI_STOMPABLE or USNJRNL_WRAPS because the existing ArtifactClass.MFT enum member covers both \$MFT and \$J/UsnJrnl per its docstring. Comment in finding.py documents the rationale.

Test plan

  • uv run pytest tests/schemas/ -v — 55 passed (54 finding + 1 artifact_class)
  • uv run ruff check verdict/ tests/ — clean
  • Pydantic v2 instantiation only; no mocks of verdict.* (§3.10)
  • Round-trip through model_dump_json / model_validate_json preserved
  • Conventional Commits format with [W1.B.5] task ID

…ats, MITRE) [W1.B.5]

Encode CLAUDE.md §3.1–§3.6 invariants at the Pydantic v2 schema layer.
The validator IS the contract: a Finding that violates §3 cannot be
constructed. SANS judges scrutinise this object end-to-end.

Fields:
* artifact_paths: list[str] min_length=2 (§3.2)
* artifact_classes: list[ArtifactClass] min_length=2 (§3.2)
* caveats_acknowledged: list[CaveatID] (§3.3)
* mitre_technique: str pattern=^T\d{4}(\.\d{3})?$ (§3.5)
* status: VerdictStatus enum — exactly six canonical values (§3.6)
* review_state: Literal[DRAFT,APPROVED,REJECTED] — orthogonal to status (§3.6)

Validators:
* _execution_requires_two_classes — T1059/T1106/T1204/T1218/T1543/T1547
  (parents and sub-techniques) require >=2 *distinct* ArtifactClass values
* _caveats_acknowledged_required — AMCACHE / SHIMCACHE / PREFETCH / MFT
  citations require their tier-1 caveats. SYSMON_PROCESSGUID_OVER_PID and
  LOGON_TYPE_3_VS_10 are correlation/field-level triggers and are
  enforced upstream (executor wrapper / correlator), not at the
  Finding-schema layer.

Adds verdict/schemas/caveat_id.py with the seven Tier-1 CaveatID enum
members per CLAUDE.md §3.3.

Tests: 54 tests in tests/schemas/test_finding.py drive every validator
through positive + negative cases (boundary, regex shapes, empty list,
duplicate-class rejection, JSON round-trip). Pydantic v2 instantiation
only — no mocks (§3.10).

@TimothyVang TimothyVang left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combined Reviewer + Auditor Report — W1.B.5 Finding Schema

Reviewer CI gate: 3 findings (all blocking)
Auditor §3 audit: all invariants correctly enforced; 3 advisory notes


CI Gate Results

Check Result Detail
ruff check (touched files) PASS All checks passed
ruff format --check (touched files) FAIL tests/schemas/test_finding.py would be reformatted
pytest tests/schemas/test_finding.py -v PASS 54/54 passed (matches claimed count)
GPG / SSH signed commits FAIL All commits on branch have %G?=N (no signature)
TDD discipline (RED then GREEN split) FAIL Single commit contains test + impl together
Task ID format PASS [W1.B.5] present and correctly formed

BLOCKING 1 — ruff format --check fails

tests/schemas/test_finding.py would be reformatted. Two issues:

  1. Missing blank line before module-level function _valid_kwargs() (needs two blank lines before it).
  2. Aligned inline comments using extra spaces ("T1218", # bare…) — ruff normalises to a single space before #.

Fix: uv run ruff format tests/schemas/test_finding.py then re-commit.


BLOCKING 2 — Unsigned commits (§3.7)

8ea8491  feat(schema): Finding with §3.1-§3.6 validators [W1.B.5]  %G?=N

git log --format="%H %G?" origin/main..HEAD returns N for every commit on the branch. CLAUDE.md §3.7: "never --no-gpg-sign". The reviewer contract requires Good signature for every commit on origin/main..HEAD. Configure git config commit.gpgsign true (or SSH signing) and re-commit.


BLOCKING 3 — Single commit contains both test and implementation (§3.7 TDD)

CLAUDE.md §3.7: "Failing test → RED → implement → GREEN → one commit per task ID."

Commit 8ea8491 adds all four files in one shot:

tests/schemas/test_finding.py  +391 lines
verdict/schemas/caveat_id.py    +40 lines
verdict/schemas/finding.py     +188 lines
verdict/schemas/__init__.py     +10 lines

This is the classic "one big commit that contains both test and impl" pattern the reviewer charter explicitly fails. A test(schema): commit introducing the failing tests must precede the feat(schema): implementation commit. The RED→GREEN boundary must be verifiable from history.


§3 Invariant Deep Audit

§3 Rule Enforced at schema layer? How Gap
§3.2 artifact_paths min_length=2 YES Field(min_length=2) on list[str]; validate_assignment=True defends post-construction mutation None
§3.2 artifact_classes min_length=2 YES Field(min_length=2) on list[ArtifactClass] None
§3.2 execution-class ≥2 distinct ArtifactClass YES _execution_requires_two_classes model_validator: len(set(self.artifact_classes)) < 2. set() collapses enum duplicates. Covers all six parents and sub-techniques via split(".", 1)[0] head match. None
§3.3 AMCACHE → AMCACHE_LASTMODIFIED_NOT_EXEC YES _CAVEAT_TRIGGERS table + _caveats_acknowledged_required validator None
§3.3 SHIMCACHE → SHIMCACHE_ORDER_CHANGED_WIN81 YES Same table None
§3.3 PREFETCH → PREFETCH_SSD_DISABLED YES Same table None
§3.3 MFT → MFT_SI_STOMPABLE or USNJRNL_WRAPS YES acceptable_caveats=(MFT_SI_STOMPABLE, USNJRNL_WRAPS); ack.intersection(acceptable_caveats) accepts either None
§3.3 LOGON_TYPE_3_VS_10 Intentionally deferred Requires EVTX_4624 (not yet in ArtifactClass enum) AND EvtxRecord.LogonType in {3,10} (field-level trigger). Comment in finding.py lines 60-65 is explicit and accurate. ADVISORY: when EVTX_4624 lands in ArtifactClass (W1.B.1 expansion), open a follow-up to wire this trigger.
§3.3 SYSMON_PROCESSGUID_OVER_PID Intentionally deferred to correlator Correct: fires on wrong-shaped correlation, not on SYSMON_1 citation. Justification in finding.py lines 66-71 is sound. None
§3.5 MITRE regex ^T\d{4}(\.\d{3})?$ YES Field(pattern=_MITRE_TECHNIQUE_RE.pattern) — Pydantic applies regex before model_validators, so execution-class validator receives a guaranteed-valid technique string None
§3.6 VerdictStatus exactly 6 values YES class VerdictStatus(str, Enum) with exactly the six canonical members; Pydantic rejects non-member strings None
§3.6 review_state orthogonal to status YES ReviewState = Literal["DRAFT","APPROVED","REJECTED"] as separate field; no coupling to VerdictStatus in any validator None
§3.10 no mocks PASS All 54 tests use real Pydantic instantiation only. No unittest.mock, no patch, no VERDICT_TEST env gate. None

Advisory Notes (non-blocking)

A1 — Dead if/pass branch in test_mitre_regex_accepts_valid_shapes (test line 338)

if good.startswith(EXECUTION_PARENT_TECHNIQUES):
    # Execution-class — keep two distinct classes (already in fixture).
    pass

This is a no-op. The fixture already satisfies the execution-class constraint. The comment is accurate but the branch is misleading because it looks like logic was omitted. Either remove it or replace with an assertion (e.g., assert len(set(kw["artifact_classes"])) >= 2).

A2 — Triple-nested tuple type annotation for _CAVEAT_TRIGGERS is hard to parse

tuple[tuple[tuple[CaveatID, ...], ArtifactClass], ...] at three nesting levels is difficult to read. A lightweight dataclass or NamedTuple with acceptable: tuple[CaveatID, ...] / trigger: ArtifactClass fields would be cheaper to extend when EVTX_4624 lands. Not a correctness issue.

A3 — MFT "either caveat satisfies" simplification

CLAUDE.md §3.3 lists MFT_SI_STOMPABLE and USNJRNL_WRAPS as separate caveats with distinct triggers ($SI timestamp use vs USN journal citation older than wrap window). Collapsing both under ArtifactClass.MFT and accepting either caveat is a correct simplification given the current enum (MFT docstring covers $MFT and $J/UsnJrnl), but if the enum ever splits MFT from USNJRNL the trigger table will need updating. The inline comment documents this clearly.


Summary

Request changes. Three blocking issues:

  1. Run uv run ruff format tests/schemas/test_finding.py — format violation.
  2. GPG/SSH sign the W1.B.5 commit per §3.7.
  3. Split into a test(schema): RED commit followed by a feat(schema): GREEN commit per §3.7 TDD contract.

The §3 schema logic is sound. Every invariant CLAUDE.md §3.2, §3.3 (four class-level triggers), §3.5, and §3.6 assigns to this schema is correctly enforced at the Pydantic v2 layer. The two deferred caveats are correctly placed upstream with accurate rationale. All 54 tests pass and probe the right boundary conditions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant