Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions api/openapi.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/reference/api-design-v4_0_1.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ GET /api/v1/scans/{scan_id}/events
- Job statuses: `queued`, `running`, `completed`, `partial`, `failed`, `canceled`.
- Task statuses: `queued`, `running`, `completed`, `failed`, `skipped`, `blocked`, `canceled`.
- A ScanResult belongs to one ScanTask, not directly to the ScanJob.
- A ScanResult carries an optional `trend`: movement against the previous result for the same test, as a `direction` (`improving`, `unchanged`, `declining`), a signed `delta`, and the id of the compared result. It tracks `grade` where the test is graded and total findings where it is not. Computed at read time; null on the first result for a test, or once the predecessor has been purged.
- Specific causes use stable machine-readable `status_reason` values. Labels, descriptions, localization, and operator guidance stay code-owned.
- A task timeout produces `failed` with a timeout `status_reason`. A job timeout terminates unfinished work; the job becomes `partial` when usable results exist, otherwise `failed`.
- `POST /scans/{scan_id}/cancel` records cancellation intent and preserves scan history. `DELETE` is never used to stop execution.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/data-model-v4_0_1.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ Constraints:
- `grade` is used only for Email, Web headers, and Web TLS tasks.
- Non-graded tasks use severity counts or per-step verdicts in `summary`.
- No cross-module composite score is stored.
- The API-level `ScanResult.trend` (`TrendDirection`: `improving`, `unchanged`, `declining`) is computed at read time from the previous comparable result and is not stored. No PostgreSQL enum carries it, like the computed verification status (§4.2).

### 8.2 `finding`

Expand Down
20 changes: 18 additions & 2 deletions src/nc3_testing_platform/domains/assets/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@
_T0 = datetime(2026, 6, 1, 8, 30, tzinfo=UTC)
_T1 = datetime(2026, 7, 31, 9, 1, 12, tzinfo=UTC)

# The challenge is younger than the proof: its answer window must still be open at
# the _T1 recheck, or a `pending` sample would carry an unanswerable challenge.
# VERIFICATION_TOKEN_TTL is env-configurable, so the invariant is asserted rather
# than assumed — a TTL short enough to close the window fails at import.
_CHALLENGE_REQUESTED_AT = datetime(2026, 7, 28, 8, 30, tzinfo=UTC)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
assert _CHALLENGE_REQUESTED_AT + VERIFICATION_TOKEN_TTL > _T1

_FEED_REVOKED_AT = datetime(2026, 7, 31, 10, 0, tzinfo=UTC)

_SUBDOMAIN_ASSET_ID = UUID("019ee1a3-0011-7a22-8b33-4c44d5e66f77")
_CHALLENGE_ID = UUID("019ee1a3-1122-7b33-9c44-5d55e6f77a88")
_FEED_ID = UUID("019ee1a3-2233-7c44-ad55-6e66f7a88b99")
Expand Down Expand Up @@ -82,9 +91,9 @@ def sample_challenge(checked: bool = False) -> VerificationChallenge:
requested_scope=VerificationScope.ZONE,
record_name=verification_record_name("example.lu"),
verification_token="verify-4f7a2c9e1b8d3056",
token_expires_at=_T0 + VERIFICATION_TOKEN_TTL,
token_expires_at=_CHALLENGE_REQUESTED_AT + VERIFICATION_TOKEN_TTL,
requested_by_user_id=USER_ID,
requested_at=_T0,
requested_at=_CHALLENGE_REQUESTED_AT,
last_recheck_at=_T1 if checked else None,
failure_code="dns.txt_record_not_found" if checked else None,
)
Expand Down Expand Up @@ -128,6 +137,13 @@ def sample_feed() -> AssetFeed:
)


def revoked_feed() -> AssetFeed:
"""The feed after revocation, revoked after its last delivery."""
feed = sample_feed()
feed.revoked_at = _FEED_REVOKED_AT
return feed


def sample_feed_created() -> AssetFeedCreated:
"""The one response that carries the plaintext token."""
return AssetFeedCreated(
Expand Down
4 changes: 1 addition & 3 deletions src/nc3_testing_platform/domains/assets/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,7 @@ async def revoke_asset_feed(asset_id: ResourceId, feed_id: ResourceId) -> AssetF
A `POST` rather than a `DELETE`, because revocation is a recorded event and the
lifecycle survives it.
"""
feed = examples.sample_feed()
feed.revoked_at = feed.created_at
return feed
return examples.revoked_feed()


@public_feed_router.get(
Expand Down
11 changes: 11 additions & 0 deletions src/nc3_testing_platform/domains/scans/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,17 @@ def sample_job(
)


def canceled_job() -> ScanJob:
"""The job once cancellation intent has taken effect.

`canceled` appears in no other sample, and the cancel response is where a
client learns the terminal shape it must render.
"""
job = sample_job(status=ScanJobStatus.CANCELED)
job.status_reason = "scan.canceled_by_user"
return job


def sample_job_detail() -> ScanJobDetail:
"""The job/task snapshot a live-progress client fetches before subscribing."""
return ScanJobDetail(**sample_job().model_dump(), tasks=sample_tasks())
Expand Down
2 changes: 1 addition & 1 deletion src/nc3_testing_platform/domains/scans/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ async def cancel_scan(scan_id: ResourceId) -> ScanJob:
safe interruption points; a canceled task cannot later produce an accepted
successful result.
"""
return examples.sample_job()
return examples.canceled_job()


@router.post(
Expand Down
1 change: 0 additions & 1 deletion src/nc3_testing_platform/domains/statements/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
@router.get(
"/statements",
summary="List active statements",
responses=problem_responses(500),
)
async def list_statements() -> list[Statement]:
"""Statements currently in force.
Expand Down