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
36 changes: 20 additions & 16 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -293,33 +293,37 @@ VulnScout lets you export and re-import the assessments you have manually create
* Sharing assessment decisions across different VulnScout instances.
* Restoring triage state in CI pipelines after a database reset.

==== Exporting Custom Assessments
==== VulnScout JSON

The `--export-custom-assessments` flag produces one OpenVEX JSON file per variant, written individually to the outputs directory (default: `.vulnscout/outputs/`).
The `--export-custom-vulnscout-data` flag exports custom assessments, CVSS scores, and time estimates as one VulnScout JSON file. It includes every variant in the selected project unless `--variant` is provided.

[source,shell]
----
# Export as individual files (one per variant)
./vulnscout --project demo --export-custom-assessments
# Export all project variants
./vulnscout --project demo --export-custom-vulnscout-data

# Export as a compressed .tar.gz archive
./vulnscout --project demo --export-custom-assessments --compress
# Export one variant
./vulnscout --project demo --variant x86 --export-custom-vulnscout-data
----

==== Importing Custom Assessments

The `--import-custom-assessments` flag reads a `.json` file, `.tar.gz` archive, or a directory of OpenVEX JSON files and replays the assessment statements into the database, matching them to existing vulnerabilities and packages.
The `--import-custom-vulnscout-data` command restores entries according to the variant metadata in the file.

[source,shell]
----
# Import from a single OpenVEX JSON file
./vulnscout --project demo --import-custom-assessments /path/to/assessments.json
./vulnscout --project demo --import-custom-vulnscout-data /path/to/custom_vulnscout_data_all.json
----

==== OpenVEX

# Import from a tar.gz archive
./vulnscout --project demo --import-custom-assessments /path/to/custom_assessments.tar.gz
The OpenVEX custom-assessment commands operate on one JSON document and require `--variant` for both import and export.

[source,shell]
----
# Export a variant
./vulnscout --project demo --variant x86 --export-custom-openvex-assessments

# Import from a directory of OpenVEX JSON files (one per variant)
./vulnscout --project demo --import-custom-assessments /path/to/assessments/
# Import into a variant
./vulnscout --project demo --variant x86 --import-custom-openvex-assessments /path/to/custom_openvex_x86.json
----

Both commands can be combined with other flags:
Expand All @@ -328,7 +332,7 @@ Both commands can be combined with other flags:
----
./vulnscout --project demo \
--add-spdx /path/to/sbom.spdx.json \
--import-custom-assessments /path/to/custom_assessments.tar.gz \
--import-custom-vulnscout-data /path/to/custom_vulnscout_data_all.json \
--export-spdx --report summary.adoc
----

Expand Down
34 changes: 21 additions & 13 deletions doc/source/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,25 +456,31 @@ Returns only user-created assessments (not scan-imported).
GET /api/assessments/review/export
```

Downloads a `.tar.gz` archive containing one OpenVEX JSON file per variant.
Downloads one OpenVEX JSON document for a selected variant.

**Query parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `author` | string | Author name in the export (default: `"Savoir-faire Linux"`) |
| `variant_id` | UUID | Required. The single variant to export. |

**Response:** Binary `.tar.gz` file (`Content-Type: application/gzip`).
**Response:** OpenVEX JSON file (`Content-Type: application/json`).

#### Import Review Assessments

```
POST /api/assessments/review/import
```

Upload an OpenVEX `.json` or `.tar.gz` file. For `.tar.gz` archives, filenames inside must match variant names.
Upload an OpenVEX `.json` file into a selected variant. The uploaded filename is not used to select the target.

**Request:** Multipart form-data with a `file` field.
**Request:** Multipart form-data with the following fields:

| Field | Type | Description |
|-------|------|-------------|
| `file` | file | OpenVEX `.json` document (required) |
| `variant_id` | UUID | Required target variant. |

**Response:**
```json
Expand All @@ -494,21 +500,24 @@ Upload an OpenVEX `.json` or `.tar.gz` file. For `.tar.gz` archives, filenames i
GET /api/assessments/review/export-custom-data
```

Exports all handmade (review) assessments, custom CVSS scores, and time estimates as a single JSON file.
Exports handmade review assessments, pending AI assessments, custom CVSS scores,
and time estimates as a single JSON file. Pending AI assessments can be reviewed
from the **AI Assessments** tab after import.

**Query parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `variant_id` | UUID | Restrict to a single variant |
| `project_id` | UUID | Restrict to all variants of a project |
| `variant_id` | UUID | Restrict to selected variants; may be repeated. |
| `project_id` | UUID | Restrict to all variants in a project. |

**Response:** JSON file download (`Content-Disposition: attachment; filename=custom_data.json`) containing:

```json
{
"version": "1.0",
"assessments": [...],
"ai_assessments": [...],
"cvss": [...],
"time_estimates": [...]
}
Expand All @@ -522,24 +531,23 @@ Returns `404` if there is no custom data to export.
POST /api/assessments/review/import-custom-data
```

Imports assessments, custom CVSS scores, and time estimates from a custom-data JSON file.
Imports assessments, pending AI assessments, custom CVSS scores, and time
estimates from a custom-data JSON file. Imported AI assessments retain their
pending state and appear in the **AI Assessments** tab.

Accepts either:

- `multipart/form-data` with a `file` field containing a `.json` file.
- `application/json` body with the custom-data payload directly.

**Query parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `variant_id` | UUID | *(optional)* Force all imported records to a specific variant |
The records keep the variant identifiers embedded in the file.

**Response:**
```json
{
"status": "success",
"assessments_imported": 12,
"ai_assessments_imported": 4,
"cvss_imported": 5,
"time_estimates_imported": 3
}
Expand Down
6 changes: 4 additions & 2 deletions doc/source/container-entrypoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ docker exec vulnscout /scan/src/entrypoint.sh --serve
| `--export-spdx` | Export project as SPDX 3.0 SBOM to `/scan/outputs/` |
| `--export-cdx` | Export project as CycloneDX 1.6 SBOM to `/scan/outputs/` |
| `--export-openvex` | Export project as OpenVEX document to `/scan/outputs/` |
| `--export-custom-assessments` | Export custom (review) assessments of the project as `.tar.gz` (or `.json` if `--variant` is specified) to `/scan/outputs/` |
| `--import-custom-assessments <path>` | Import custom assessments from `.json` or `.tar.gz` |
| `--export-custom-vulnscout-data` | Export custom VulnScout JSON data for all project variants, or the selected `--variant`, to `/scan/outputs/` |
| `--import-custom-vulnscout-data <path>` | Import custom VulnScout JSON data using the variant metadata in the file |
| `--export-custom-openvex-assessments` | Export custom assessments for the selected `--variant` as an OpenVEX `.json` file |
| `--import-custom-openvex-assessments <path>` | Import an OpenVEX `.json` file into the selected `--variant` |
| `--match-condition <expr>` | Exit with code 2 if expression matches any vulnerability. Incompatible with `--serve` |
| `--delete-scan <id>` | Delete a past scan by its ID |

Expand Down
4 changes: 2 additions & 2 deletions doc/source/interactive-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,5 +298,5 @@ The toolbar mirrors the vulnerability table's search bar. Filters are available

Two buttons in the toolbar handle review portability:

- **Import Review**: accepts either an OpenVEX file (JSON or `.tar.gz`) or a VulnScout custom-data JSON file and merges its contents into the current project. OpenVEX files import assessments only; custom-data files additionally restore custom CVSS scores and time estimates. This is useful for receiving triage decisions from another team or migrating data between VulnScout instances.
- **Export Review**: downloads all handmade assessments, custom CVSS scores, and time estimates as a single JSON file. The export captures the full set of user-created data so it can be shared, archived, or loaded into another VulnScout deployment.
- **Import Review**: choose either an OpenVEX JSON document or a VulnScout JSON file. OpenVEX imports assessments into one selected variant. VulnScout JSON restores all included assessments, custom CVSS scores, and time estimates using the variants recorded in the file.
- **Export Review**: choose VulnScout JSON to export assessments, custom CVSS scores, and time estimates for selected variants, or OpenVEX to export assessments for one selected variant as a JSON document.
36 changes: 17 additions & 19 deletions doc/source/vulnscout-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,42 +312,40 @@ VulnScout lets you export and re-import the assessments you have manually create
- Sharing assessment decisions across different VulnScout instances.
- Restoring triage state in CI pipelines after a database reset.

### Exporting Custom Assessments
### VulnScout JSON

The `--export-custom-assessments` flag produces one OpenVEX JSON file per variant, written directly to the outputs directory:
The `--export-custom-vulnscout-data` flag exports custom assessments, pending AI
assessments, CVSS scores, and time estimates as one VulnScout JSON file. It
includes every variant in the selected project unless `--variant` is provided.
After import, pending AI assessments are available from the Review page's **AI
Assessments** tab for approval or rejection.

```bash
./vulnscout --project demo --export-custom-assessments
./vulnscout --project demo --export-custom-vulnscout-data
```

To bundle the exported files into a `.tar.gz` archive instead, add the `--compress` flag:
Export one variant:

```bash
./vulnscout --project demo --export-custom-assessments --compress
./vulnscout --project demo --variant x86 --export-custom-vulnscout-data
```

You can also use the `--variant` flag to export only a single variant:
The `--import-custom-vulnscout-data` command restores entries according to the variant metadata stored in the file:

```bash
./vulnscout --project demo --variant x86 --export-custom-assessments
./vulnscout --project demo --import-custom-vulnscout-data /path/to/custom_vulnscout_data_all.json
```

### Importing Custom Assessments
### OpenVEX

The `--import-custom-assessments` flag reads a `.json` file, `.tar.gz` archive, or a directory of OpenVEX JSON files and replays the assessment statements into the database. If `--variant` is not specified, the variant is inferred from the file name.
OpenVEX custom-assessment transfers operate on one JSON document and require `--variant` for both import and export.

```bash
# Import from a single OpenVEX JSON file
./vulnscout --project demo --variant x86 --import-custom-assessments /path/to/assessments.json

# Import from a single OpenVEX JSON file without specifying the variant
./vulnscout --project demo --import-custom-assessments /path/to/assessments/x86.json

# Import from a tar.gz archive
./vulnscout --project demo --import-custom-assessments /path/to/custom_assessments.tar.gz
# Export a variant
./vulnscout --project demo --variant x86 --export-custom-openvex-assessments

# Import from a directory of OpenVEX JSON files (one per variant)
./vulnscout --project demo --import-custom-assessments /path/to/assessments/
# Import into a variant
./vulnscout --project demo --variant x86 --import-custom-openvex-assessments /path/to/custom_openvex_x86.json
```

---
Expand Down
106 changes: 106 additions & 0 deletions frontend/src/components/ReviewTransferModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import type { Variant } from '../handlers/variant';

type Props = {
mode: 'import' | 'export';
variants: Variant[];
selectedVariantIds: string[];
transferFormat: 'custom' | 'openvex';
onSelectedVariantIdsChange: (ids: string[]) => void;
onTransferFormatChange: (format: 'custom' | 'openvex') => void;
onConfirm: () => void;
onCancel: () => void;
};

function ReviewTransferModal({
mode,
variants,
selectedVariantIds,
transferFormat,
onSelectedVariantIdsChange,
onTransferFormatChange,
onConfirm,
onCancel,
}: Readonly<Props>) {
const title = mode === 'export' ? 'Export review data' : 'Import review data';
const isOpenVex = transferFormat === 'openvex';
const needsVariantSelection = isOpenVex || mode === 'export';
const supportsMultipleVariants = !isOpenVex;

useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onCancel();
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [onCancel]);

const toggleVariant = (variantId: string) => {
onSelectedVariantIdsChange(
selectedVariantIds.includes(variantId)
? selectedVariantIds.filter(id => id !== variantId)
: [...selectedVariantIds, variantId],
);
};

return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 p-4" role="dialog" aria-modal="true" aria-labelledby="review-transfer-title">
<div className="w-full max-w-lg rounded-lg border border-gray-600 bg-gray-800 shadow-xl">
<div className="flex items-center justify-between border-b border-gray-600 px-5 py-4">
<h2 id="review-transfer-title" className="text-lg font-semibold text-white">{title}</h2>
<button type="button" onClick={onCancel} aria-label="Close" className="h-8 w-8 text-gray-300 hover:text-white">
<FontAwesomeIcon icon={faXmark} />
</button>
</div>

<div className="space-y-5 p-5">
<fieldset>
<legend className="mb-2 text-sm font-semibold text-gray-200">Format</legend>
<div className="grid grid-cols-2 gap-3">
<label className="flex cursor-pointer items-start gap-2 rounded border border-gray-600 p-3 text-gray-100">
<input type="radio" name="review-transfer-format" checked={transferFormat === 'custom'} onChange={() => onTransferFormatChange('custom')} />
<span><span className="block font-medium">VulnScout JSON</span><span className="text-xs text-gray-400">{mode === 'export' ? 'Assessments, CVSS, and time estimates' : 'Uses the variants recorded in the file'}</span></span>
</label>
<label className="flex cursor-pointer items-start gap-2 rounded border border-gray-600 p-3 text-gray-100">
<input type="radio" name="review-transfer-format" checked={isOpenVex} onChange={() => onTransferFormatChange('openvex')} />
<span><span className="block font-medium">OpenVEX</span><span className="text-xs text-gray-400">One variant in a JSON document</span></span>
</label>
</div>
</fieldset>

{needsVariantSelection && (
<fieldset>
<legend className="float-left text-sm font-semibold text-gray-200">{supportsMultipleVariants ? 'Variants' : 'Variant'}</legend>
{supportsMultipleVariants && (
<div className="mb-2 flex items-center justify-end">
<button type="button" className="text-sm text-cyan-300 hover:text-cyan-200" onClick={() => onSelectedVariantIdsChange(selectedVariantIds.length === variants.length ? [] : variants.map(variant => variant.id))}>
{selectedVariantIds.length === variants.length ? 'Clear all' : 'Select all'}
</button>
</div>
)}
<div className="clear-both max-h-64 space-y-1 overflow-y-auto rounded border border-gray-600 p-2">
{variants.map(variant => (
<label key={variant.id} className="flex cursor-pointer items-center gap-3 rounded px-2 py-2 text-sm text-gray-100 hover:bg-gray-700">
<input type={supportsMultipleVariants ? 'checkbox' : 'radio'} name="review-openvex-variant" checked={supportsMultipleVariants ? selectedVariantIds.includes(variant.id) : selectedVariantIds[0] === variant.id} onChange={() => supportsMultipleVariants ? toggleVariant(variant.id) : onSelectedVariantIdsChange([variant.id])} />
{variant.name}
</label>
))}
</div>
</fieldset>
)}
</div>

<div className="flex justify-end gap-3 border-t border-gray-600 px-5 py-4">
<button type="button" onClick={onCancel} className="rounded border border-gray-500 px-4 py-2 text-sm text-gray-200 hover:bg-gray-700">Cancel</button>
<button type="button" onClick={onConfirm} disabled={needsVariantSelection && (isOpenVex ? selectedVariantIds.length !== 1 : selectedVariantIds.length === 0)} className="rounded bg-green-700 px-4 py-2 text-sm font-medium text-white hover:bg-green-600 disabled:cursor-not-allowed disabled:opacity-50">
{mode === 'export' ? 'Export' : 'Choose file'}
</button>
</div>
</div>
</div>
);
}

export default ReviewTransferModal;
18 changes: 11 additions & 7 deletions frontend/src/helpers/exportJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ export function formatTimestampForFilename(date?: Date | string): string {
*/
export function downloadJson(data: unknown, filename: string): void {
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
downloadBlob(new Blob([json], { type: 'application/json' }), filename);
}

/** Trigger a download for a response body that is already encoded. */
export function downloadBlob(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(url);
}
Loading
Loading