Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ working directory. Environment variables override the built-in defaults.
| `GREENBONE_WAS_SCAN_WORKER_COUNT` | `1` | Maximum number of concurrently running scan workers. The value must be greater than `0`. |
| `GREENBONE_WAS_SCAN_ALERT_POLL_INTERVAL_SECONDS` | `10` | Interval, in seconds, between ZAP alert polling attempts during active scans. The value must be greater than `0`. |
| `GREENBONE_WAS_SCAN_STOP_GRACE_PERIOD_SECONDS` | `300` | Grace period, in seconds, to wait for a running scan to stop before forcing it to failed. The value must be greater than `0`. |
| `GREENBONE_WAS_SCAN_AJAX_SPIDER_TIMEOUT_GRACE_PERIOD_SECONDS` | `60` | Grace period, in seconds, added to the scan-level `ajax_spider_timeout` preference before sending a local AJAX spider stop request. |
| `GREENBONE_WAS_SCAN_PHASE_STOP_STATUS_CHANGE_TIMEOUT_SECONDS` | `60` | Time limit, in seconds, for waiting on a scan phase status change after a local stop request; when exceeded, WAS logs a warning and continues to the next phase. The value must be greater than `0`. |
| `GREENBONE_WAS_SCAN_RETRY_MAX_RETRIES` | `10` | Maximum number of retry attempts for transient ZAP or storage failures. |
| `GREENBONE_WAS_SCAN_RETRY_MAX_DELAY_SECONDS` | `60` | Maximum backoff delay, in seconds, between retry attempts. The value must be greater than `0`. |

Expand Down
4 changes: 1 addition & 3 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,11 @@ highlight = "all"
# upstream transitions passing while causing any newly introduced duplicate
# version to fail CI.
skip = [
{ name = "foldhash", version = "0.1.5" },
{ name = "getrandom", version = "0.2.17" },
{ name = "hashbrown", version = "0.14.5" },
{ name = "hashbrown", version = "0.15.5" },
{ name = "hashbrown", version = "0.16.1" },
{ name = "hashlink", version = "0.10.0" },
{ name = "windows-sys", version = "0.52.0" },
{ name = "tower-http", version = "0.6.11"},
{ name = "wit-bindgen", version = "0.51.0" },
]

Expand Down
38 changes: 23 additions & 15 deletions doc/openapi-reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ components:
type: "string"
type:
description: "Type of the preference"
type: "string"
name:
description: "Display name for the preference"
type: "string"
Expand All @@ -711,8 +712,14 @@ components:
description: "Default value for scans"
type: "string"
values:
description: "Allowed values"
description: "Allowed values as semicolon-separated string"
type: "string"
required:
- "id"
- "type"
- "name"
- "description"
- "default"

examples:
scan_simple:
Expand Down Expand Up @@ -793,9 +800,8 @@ components:
},
"scan_preferences":
[
{ "id": "target_port", "value": "443" },
{ "id": "use_https", "value": "1" },
{ "id": "profile", "value": "fast_scan" },
{ "id": "scan_mode", "value": "safe" },
{ "id": "ajax_spider_timeout", "value": "0" },
],
"vts":
[
Expand Down Expand Up @@ -874,9 +880,8 @@ components:
},
"scan_preferences":
[
{ "id": "target_port", "value": "443" },
{ "id": "use_https", "value": "1" },
{ "id": "profile", "value": "fast_scan" },
{ "id": "scan_mode", "value": "safe" },
{ "id": "ajax_spider_timeout", "value": "0" },
],
"vts":
[
Expand Down Expand Up @@ -1098,15 +1103,18 @@ components:
value:
[
{
"id": "optimize_test",
"name": "Optimize Test",
"default": true,
"description": "By default, optimize_test is enabled which means openvas does trust the remote host banners and is only launching plugins against the services they have been designed to check. For example it will check a web server claiming to be IIS only for IIS related flaws but will skip plugins testing for Apache flaws, and so on. This default behavior is used to optimize the scanning performance and to avoid false positives. If you are not sure that the banners of the remote host have been tampered with, you can disable this option.",
"id": "scan_mode",
"type": "enum",
"name": "Scan Mode",
"default": "safe",
"values": "safe;active",
"description": "Scan mode: 'safe' disables active scans, 'active' enables active scans.",
},
{
"id": "plugins_timeout",
"name": "Plugins Timeout",
"default": 5,
"description": "This is the maximum lifetime, in seconds of a plugin. It may happen that some plugins are slow because of the way they are written or the way the remote server behaves. This option allows you to make sure your scan is never caught in an endless loop because of a non-finishing plugin. Doesn't affect ACT_SCANNER plugins, use 'ACT_SCANNER plugins timeout' for them instead.",
"id": "ajax_spider_timeout",
"type": "integer",
"name": "AJAX Spider Timeout",
"default": "3600",
"description": "Scan-level AJAX spider timeout in seconds, applied per target. Value 0 means unlimited.",
},
]
15 changes: 10 additions & 5 deletions doc/specs/scan-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,13 @@ If the scan is failed there should be an attempt to stop any spider or active sc

Once the context is set up, the worker runs the AJAX spider for each target URL and updates the progress. The AJAX spider timeout is taken from the preferences passed to `create_scan`.

After the spider is finished, the worker runs active scans against the target URLs, updating the progress. The active scan timeout is likewise taken from the `create_scan` preferences.
Before each AJAX spider run, the worker sets the ZAP AJAX spider max-duration option (`ajaxSpider/setOptionMaxDuration`) using the effective `ajax_spider_timeout` value.

If either the AJAX spider or active scan times out, a warning is logged and an error result is added to the storage.
If `ajax_spider_timeout` is omitted, the default timeout is 3600 seconds (60 minutes). If it is set to `0`, the timeout is treated as unlimited.

After the spider is finished, the worker either runs the active scan stage (`scan_mode=active`) or skips it (`scan_mode=safe`).

After active-scan completion (or immediately after spider in safe mode), the worker enters a temporary passive-scan placeholder stage for progress tracking and marks it done after a short fixed wait.

Alert polling and context operations do not have dedicated timeouts; transient failures are handled by the general retry mechanism.

Expand Down Expand Up @@ -258,12 +262,12 @@ If all worker slots are occupied, additional started scans remain in `requested`
## Progress model

Progress is represented internally using the per-target variables:
- A state enum (`pending`, `running`, `done`) for each stage of the scan (`spider`, `active_scan`).
- The last ZAP state for each stage (`running` or `stopped` for spider, a percentage for active scan).
- A state enum (`pending`, `running`, `done`) for each stage of the scan (`spider`, `active_scan`, `passive_scan`).
- The last ZAP state for spider (`running` or `stopped`) and percentages for active and passive scan.
- A per-host progress percentage calculated as follows:
- if the spider stage is not started yet, `progress = 0`
- if the spider stage is started but not finished yet, `progress = 1`
- once the spider stage is finished, `progress = floor(25 + 0.75 * active_scan_percentage)`
- once the spider stage is finished, `progress = floor(25 + 0.7 * active_scan_percentage + 0.05 * passive_scan_percentage)`

For the HTTP API representation, progress is exposed as `host_info`:
- `all`: total number of hosts in the scan target scope.
Expand Down Expand Up @@ -308,6 +312,7 @@ In-memory SQLite is reserved for the storage module's own unit tests. Those stor
- Error paths that result in `failed` status.
- Startup recovery: non-terminal scans are set to `failed` on service restart.
- Alert-to-result mapping, including `Informational -> log`, all other alert risk levels -> `alarm`, URL-derived host and port extraction, and invalid alert URL fallback behavior.
- Preference-driven worker behavior, including `scan_mode=safe` active-stage skip, AJAX spider timeout option updates (including `0` for unlimited and default `3600` seconds), and passive-scan progress stage transitions.

## Notes and open questions

Expand Down
156 changes: 156 additions & 0 deletions doc/specs/scan-preferences.plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Scan Preferences Implementation Plan

This plan adds scanner preference support to the scan service and aligns the public API contract with `doc/openapi-reference.yml`. The initial preference set contains a `scan_mode` enum preference with values `safe` (disables active scans) and `active` (enables active scans), defaulting to `safe`, plus an `ajax_spider_timeout` preference defined once per scan, with its limit enforced per target.

## Target Files

- `src/scan/mod.rs`
- `src/scan/preferences.rs` (new)
- `src/scan/errors.rs`
- `src/scan/service.rs`
- `src/scan/service_tests.rs`
- `src/scan/progress.rs`
- `src/scan/progress_tests.rs`
- `src/scan/worker.rs`
- `src/scan/worker_tests.rs`
- `src/zapclient/ajaxspider.rs`
- `src/zapclient/ajaxspider_tests.rs`
- `src/zapclient/ascan.rs`
- `src/zapclient/ascan_tests.rs`
- `src/api/dto/scans.rs`
- `src/api/dto/scans_tests.rs`
- `src/api/scans.rs`
- `src/api/scans_tests.rs`
- `src/api/openapi.rs`
- `src/api/openapi_tests.rs`
- `doc/openapi-reference.yml`
- `doc/specs/scan-module.md`

## Phase 1: Preference Registry and Defaults

Status: Implemented

Add a scan-owned preference registry that defines the supported scanner preferences.

- Add a new module under `src/scan/` for scanner preferences.
- Store preference metadata and default values in one place.
- Define `scan_mode` as an enum preference with values `safe` and `active`, with default `safe`.
- Define an `ajax_spider_timeout` preference with units in seconds, configured at scan level and applied per target; value `0` means unlimited timeout.
- Keep the preference registry independent from storage and HTTP transport concerns.

## Phase 2: API Contract Alignment

Status: Implemented

Update the API DTOs and OpenAPI wiring early so the public contract is stable before service behavior is finalized.

- Ensure `POST /scans` continues to accept preference overrides.
- Ensure `GET /scans/preferences` returns the documented preference list.
- Update OpenAPI annotations and `doc/openapi-reference.yml` so the documented preference list includes `scan_mode` with allowed values `safe|active` and default `safe`, plus `ajax_spider_timeout` described as a scan-level setting enforced per target.

## Phase 3: Scan Service Integration

Status: Implemented

Teach the scan service to implement the phase-2 API contract for defaults and scan creation.

- Return the default preference list from `get_default_preferences`.
- Pass `scan_mode` and `ajax_spider_timeout` from scan creation input into the scan service preference resolution path.
- Validate preference input during scan creation.
- Allow unknown preference ids but emit a warning message for each unknown preference.
- Validate `scan_mode` values against the allowed enum values (`safe`, `active`).
- Validate the AJAX spider time limit as a non-negative integer number of seconds, where `0` means unlimited.
- Persist the scan with the effective preference set.
- Keep the persisted scan record shape unchanged.

## Phase 4: Scan Mode Behavior

Status: Implemented

Implement mode-driven active-scan behavior using `scan_mode` in scan preferences.

- Resolve the effective `scan_mode` value once per scan from the scan-level preferences passed through the scan service.
- For each target, if `scan_mode=safe`, skip the active-scan stage and proceed to the post-active-scan flow as if that stage completed normally.
- For each target, if `scan_mode=active`, run the active-scan stage as normal.
- Ensure the mode behavior applies consistently for all targets in the scan.
- Emit a debug log indicating that active scan was skipped due to `scan_mode=safe`, including scan id and target.
- Ensure progress/state transitions remain valid in both `safe` and `active` modes.

## Phase 5: Passive Scan Progress Stage

Status: Implemented

Add a passive-scan target progress stage that follows active scan (or follows spider directly when `scan_mode=safe`) and contributes to overall percentage.

- Extend target progress state to include a passive-scan stage entered after active-scan completion (or immediately after spider when `scan_mode=safe`).
- Introduce `passive_scan_percentage` in target progress tracking and keep existing stage-state semantics (`pending`, `running`, `done`).
- Implement a temporary placeholder for passive scanning: wait 5 seconds per target, then mark passive-scan stage as done.
- Treat correct passive-scan progress integration beyond the placeholder wait as out of scope for this plan.
- Update overall percentage calculation so the existing spider-crawl contribution remains included:
- old post-spider formula: `25 + 0.75 * active_scan_percentage`
- new post-spider formula: `25 + 0.7 * active_scan_percentage + 0.05 * passive_scan_percentage`
- Keep other parts of progress calculation unchanged.

## Phase 6: AJAX Spider Limit Enforcement

Status: Implemented

Implement runtime enforcement of the scan-level AJAX spider limit with per-target timers.

- Resolve `ajax_spider_timeout` once for a scan before target iteration begins.
- Start a fresh timer for each target when its AJAX spider stage starts.
- Pass the configured limit to ZAP AJAX spider start APIs if supported by the request contract.
- Enforce timeout locally in worker control flow so per-target limits are guaranteed even if ZAP-side timeout behavior changes.
- Treat `ajax_spider_timeout=0` as unlimited and skip local timeout enforcement while still running normal spider stage polling.
- When a target exceeds its limit:
- stop the target's AJAX spider activity,
- emit an info log with scan id, target, configured limit, and elapsed seconds,
- continue the remaining scan steps for the same target as if the spider stage finished normally,
- continue processing remaining targets unless a stop request or terminal worker error occurs.
- Keep timeout accounting independent per target (no shared global budget across all targets).
- Ensure the default behavior (when preference is omitted) is deterministic and documented.

## Phase 7: Tests and Documentation

Status: Implemented

Add focused coverage for the new preference behavior and update any stale scan-module docs.

- Cover preference serialization and deserialization.
- Cover scan-service default preference behavior.
- Cover warning behavior for unknown preference ids.
- Cover scan-service handling of scan-level `ajax_spider_timeout` values.
- Cover scan-service propagation and resolution of `scan_mode` for runtime behavior.
- Cover worker behavior for `scan_mode=safe`, including active-scan stage skip, debug logging, and valid per-target flow completion.
- Cover worker behavior for `scan_mode=active`, including normal active-scan execution.
- Cover worker enforcement behavior for per-target AJAX spider limits, including timeout, info logging, continuation of same-target follow-up stages, and continuation to next target.
- Cover progress-model behavior for passive-scan stage transitions and placeholder 5-second completion.
- Cover updated percentage formula using `25 + 0.7 * active_scan_percentage + 0.05 * passive_scan_percentage`.
- Cover API handler delegation and OpenAPI schema coverage.
- Update `doc/specs/scan-module.md` if it still describes the placeholder preference behavior.

## Verification

- Run `cargo fmt --all -- --check`.
- Run targeted unit tests for `src/api/dto/scans.rs`, `src/api/scans.rs`, `src/api/openapi.rs`, and `src/scan/service.rs`.
- Run targeted worker and ZAP client tests for `scan_mode=safe` skip behavior and `scan_mode=active` active-scan behavior.
- Run targeted worker and ZAP client tests for AJAX timeout enforcement and timeout signaling.
- Run targeted progress tests for passive-scan stage behavior and revised percentage math.
- Run the broader scan and API test suites after the focused tests pass.
- Inspect the generated OpenAPI output to confirm `/scans/preferences` returns the documented array shape and that `scan_mode` (with values `safe|active`) plus `ajax_spider_timeout` appear with correct defaults and descriptions.

## Decisions

- Keep the current persisted scan record shape unchanged.
- Treat the preference registry as the source of truth for defaults and documented metadata.
- Allow unknown preference ids and emit warning messages instead of rejecting scan creation.
- Pass `scan_mode` through scan creation to the scan service.
- In `scan_mode=safe`, skip active-scan stage; in `scan_mode=active`, run active scan.
- Treat the AJAX spider time limit as a scan-level seconds value and enforce it independently for each target in the scan.
- Treat `ajax_spider_timeout=0` as unlimited.
- Introduce passive-scan progress as a temporary 5-second per-target placeholder stage after active scan (or after spider when `scan_mode=safe`). Full handling of the passive scan is out of scope for this plan.

## Noted Deviations

- Deviation from Phase 6 local enforcement: the worker no longer stops AJAX spider scans locally when timeout is exceeded. Instead, it sets the ZAP AJAX spider option (`ajaxSpider/setOptionMaxDuration`) before each spider run and relies on ZAP-side timeout behavior.
- Deviation from original default timeout: the default `ajax_spider_timeout` is `3600` seconds (60 minutes) instead of `0`.
27 changes: 26 additions & 1 deletion src/api/dto/scans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,32 @@ pub enum ResultType {
/// Response body for GET /scans/preferences – available scanner preferences.
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[cfg_attr(feature = "api-docs", derive(utoipa::ToSchema))]
pub struct PreferencesResponse {}
#[serde(transparent)]
pub struct PreferencesResponse(
/// Available scanner preferences and their default values.
pub Vec<ScannerPreferenceMetadata>,
);

/// Metadata entry returned by GET /scans/preferences.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "api-docs", derive(utoipa::ToSchema))]
pub struct ScannerPreferenceMetadata {
/// Preference identifier.
pub id: String,
/// Preference value type (for example: enum, integer).
#[serde(rename = "type")]
pub preference_type: String,
/// Display name for the preference.
pub name: String,
/// Human-readable preference description.
pub description: String,
/// Default value for new scans.
#[serde(rename = "default")]
pub default_value: String,
/// Allowed values for constrained preference types, represented as a semicolon-separated string.
#[serde(skip_serializing_if = "Option::is_none")]
pub values: Option<String>,
}

/// Response body for GET /scans/{id} – full scan details.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
Expand Down
Loading