Skip to content

Expose what a bulk export needs from the assessment API - #149

Open
summitt wants to merge 1 commit into
mainfrom
feat/migration-export-api
Open

Expose what a bulk export needs from the assessment API#149
summitt wants to merge 1 commit into
mainfrom
feat/migration-export-api

Conversation

@summitt

@summitt summitt commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Reading a whole FACTION instance through the API — to migrate it, archive it, or feed another system — is not currently possible. Three gaps stand in the way, and this closes them. Everything here is additive; no existing response loses a field.

A finding's state was not in the payload

VulnerabilityDTO carried severity, text and CVSS, but not whether the finding was open. Added Status, Opened, Closed, DevClosed, StagingClosed, and the category (Category, CategoryId). Dates cross the wire as epoch-millisecond strings, matching the existing Created. Unset fields stay out of the payload rather than appearing as null.

Nothing listed every assessment

/queue returns open assessments assigned to the caller; /completed needs a date range. A full inventory meant guessing at the union of two partial views, and still missing in-progress work assigned to someone else.

GET /assessments/all returns what the caller can see in one call, using the same scoping the UI applies — managers with unrestricted access get everything, everyone else gets their own subset.

The report download served one variant, with no way to choose

It preferred the PDF and fell back to whatever was first. Now:

  • GET /assessments/report/{aid}?type=docx|pdf&retest=true picks a variant and can reach the retest report. With no type the old behaviour is unchanged.
  • GET /assessments/report/{aid}/info lists what a report actually holds, so a caller can decide before pulling a multi-megabyte blob. It also reports whether the PDF is password protected — the password itself is deliberately not exposed.

A named type that is absent returns 404 rather than substituting another format. Handing back a PDF to a caller that asked for a DOCX would get it filed as the wrong document type on the far end. Downloads now also carry a real Content-Type instead of defaulting to octet-stream.

GET /users/all rounds this out: the directory is how an integration resolves the user ids that appear on assessments, and there was no way to read it. Admin or manager only; password hashes and tokens are never included.

A live NPE this surfaced

Vulnerability.getAssessmentId() is a primitive long getter over a nullable Long field, so any finding not attached to an assessment threw on read. That is not limited to the new endpoints — it takes down the existing /assessments/vulns/{aid} too.

It now returns 0, the "no assessment" value callers already test for (AppBootstrapListener does getAssessmentId() > 0), and the DTO omits the field rather than pointing consumers at assessment 0. AssessmentDTO's start/end are guarded for the same reason: a bulk listing sweeps up drafts that never had dates set, and one of them would otherwise fail the whole page.

Testing

New MigrationExportAPITest — 16 tests over variant selection (including legacy reports with no variant list, and the refusal to substitute a format), the info listing, retest downloads, and the DTO lifecycle fields. Full suite: 927 pass, 0 failures.

🤖 Generated with Claude Code

https://claude.ai/code/session_01S72uFdcjbJfhgEwwWxnfKX

Migrating a FACTION instance elsewhere means reading the whole inventory, and
three gaps made that impossible through the API:

- VulnerabilityDTO carried a finding's severity but not its state. Added
  Status, Opened, Closed, DevClosed, StagingClosed and the category, so a
  consumer can reproduce a finding rather than just describe it.
- Nothing listed every assessment. /queue is open assessments assigned to the
  caller and /completed wants a date range, so a full inventory meant guessing
  at the union of two partial views. GET /assessments/all returns what the
  caller can see, in one call.
- The report download served one variant, preferring the PDF with no way to
  ask for anything else. It now takes type=docx|pdf and retest=true, and
  /report/{aid}/info lists what a report actually holds so a caller can choose
  before pulling a blob. A named type that is absent 404s rather than
  substituting another format, which would otherwise be filed as the wrong
  document type on the far end.

GET /users/all rounds this out: the directory is how an integration resolves
the user ids that appear on assessments, and there was no way to read it.

Also fixes an NPE this surfaced. Vulnerability.getAssessmentId() is a
primitive getter over a nullable Long, so any finding not attached to an
assessment threw on read — that took down /assessments/vulns/{aid}, not just
the new paths. It now returns 0, the "no assessment" value callers already
test for (see AppBootstrapListener), and the DTO omits the field rather than
pointing consumers at assessment 0. AssessmentDTO's start/end are guarded for
the same reason: a bulk listing sweeps up drafts that never had dates set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S72uFdcjbJfhgEwwWxnfKX
if (finalReport == null) {
return Response.status(404).entity(String.format(Support.ERROR, "No final report available for this assessment")).build();
}
return invokeDownloadReport(u, assessment, aid, type, retest);
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.

2 participants