Skip to main content

termivar_scanner/
web_actions.rs

1//! Transport-neutral semantic action identities for standard web profiles.
2//!
3//! ## Runtime scope
4//!
5//! - **Build:** always/default.
6//! - **Execution:** Surface B (deterministic decision runtime).
7//! - **Default `termivar scan`:** yes, through `StandardWebDecisionRuntime`.
8//! - **Support:** implemented and tested.
9//!
10//! See `docs/internals/runtime-map.md`.
11//!
12//! Planning, verification, and execution consume this catalog as sibling
13//! layers. The catalog defines stable meaning and routing identities but owns
14//! no HTTP method, client, executor, or runtime policy.
15
16use serde::{Deserialize, Serialize};
17
18mod native_review;
19
20#[cfg(all(feature = "scanning", feature = "authorization-review"))]
21pub(crate) use native_review::authorization_review_phase_terminal_predicate;
22#[cfg(all(feature = "scanning", feature = "openapi-review"))]
23pub(crate) use native_review::openapi_review_phase_terminal_predicate;
24#[cfg(feature = "scanning")]
25pub(crate) use native_review::{
26    native_web_review_response_marker_predicate, NATIVE_WEB_REVIEW_EVIDENCE_NAMESPACE,
27    NATIVE_WEB_REVIEW_RESPONSE_MARKER,
28};
29#[cfg(all(feature = "scanning", feature = "rest-review"))]
30pub(crate) use native_review::{
31    rest_review_catalog_ready_predicate, rest_review_phase_terminal_predicate,
32};
33#[cfg(all(feature = "scanning", feature = "ssrf-oast-review"))]
34pub(crate) use native_review::{
35    ssrf_oast_review_candidate_ready_predicate, ssrf_oast_review_phase_terminal_predicate,
36};
37pub use native_review::{
38    NativeWebReviewActionKind, NativeWebReviewDifferentialInput, NativeWebReviewRequestLeg,
39    NATIVE_WEB_REVIEW_ACTION_COUNT, NATIVE_WEB_REVIEW_ACTIVE_REQUESTS_PER_CASE,
40    NATIVE_WEB_REVIEW_REQUESTS_PER_CASE,
41};
42
43/// Number of semantic actions in the standard web catalog.
44pub const STANDARD_WEB_ACTION_COUNT: usize = 9;
45
46/// Number of semantic actions supported by the built-in discovery capability.
47pub const STANDARD_WEB_DISCOVERY_ACTION_COUNT: usize = 8;
48
49/// Stable semantic action kinds shared by standard web profiles.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52#[non_exhaustive]
53pub enum StandardWebActionKind {
54    /// Inspect nginx-specific configuration and routing behavior.
55    NginxConfiguration,
56    /// Inspect Apache HTTP Server configuration and routing behavior.
57    ApacheConfiguration,
58    /// Discover PHP request inputs before deeper testing.
59    PhpInputDiscovery,
60    /// Enumerate Laravel routes with a low-risk discovery executor.
61    LaravelRouteDiscovery,
62    /// Analyze inputs discovered on Laravel routes.
63    LaravelInputAnalysis,
64    /// Enumerate Livewire component boundaries.
65    LivewireComponentDiscovery,
66    /// Observe a Sanctum-compatible session/CSRF cookie-name convention.
67    ///
68    /// The cookie convention motivates this action but does not independently
69    /// prove that Sanctum authentication is installed or active.
70    SanctumAuthBoundary,
71    /// Analyze an advertised HTTP Basic authentication boundary.
72    HttpBasicAuthBoundary,
73    /// Analyze an advertised HTTP Bearer authentication boundary.
74    HttpBearerAuthBoundary,
75}
76
77impl StandardWebActionKind {
78    /// Returns every standard kind in stable declaration order.
79    pub const fn all() -> [Self; STANDARD_WEB_ACTION_COUNT] {
80        [
81            Self::NginxConfiguration,
82            Self::ApacheConfiguration,
83            Self::PhpInputDiscovery,
84            Self::LaravelRouteDiscovery,
85            Self::LaravelInputAnalysis,
86            Self::LivewireComponentDiscovery,
87            Self::SanctumAuthBoundary,
88            Self::HttpBasicAuthBoundary,
89            Self::HttpBearerAuthBoundary,
90        ]
91    }
92
93    /// Returns the semantic action subset supported by discovery executors.
94    ///
95    /// HTTP method selection and executor construction remain in the execution
96    /// layer; this list only declares the capability boundary shared with
97    /// verification.
98    pub const fn discovery() -> [Self; STANDARD_WEB_DISCOVERY_ACTION_COUNT] {
99        [
100            Self::NginxConfiguration,
101            Self::ApacheConfiguration,
102            Self::PhpInputDiscovery,
103            Self::LaravelRouteDiscovery,
104            Self::LivewireComponentDiscovery,
105            Self::SanctumAuthBoundary,
106            Self::HttpBasicAuthBoundary,
107            Self::HttpBearerAuthBoundary,
108        ]
109    }
110
111    /// Returns the stable planner action identity.
112    pub const fn action_id(self) -> &'static str {
113        match self {
114            Self::NginxConfiguration => "web.action.nginx.configuration",
115            Self::ApacheConfiguration => "web.action.apache.configuration",
116            Self::PhpInputDiscovery => "web.action.php.input-discovery",
117            Self::LaravelRouteDiscovery => "web.action.laravel.route-discovery",
118            Self::LaravelInputAnalysis => "web.action.laravel.input-analysis",
119            Self::LivewireComponentDiscovery => "web.action.livewire.component-discovery",
120            Self::SanctumAuthBoundary => "web.action.sanctum.auth-boundary",
121            Self::HttpBasicAuthBoundary => "web.action.http-basic.auth-boundary",
122            Self::HttpBearerAuthBoundary => "web.action.http-bearer.auth-boundary",
123        }
124    }
125
126    /// Returns the stable opaque route identity for an action executor.
127    ///
128    /// The identity does not select a transport or implementation. Runtime
129    /// composition binds it to an executor outside this semantic catalog.
130    pub const fn executor_id(self) -> &'static str {
131        match self {
132            Self::NginxConfiguration => "web.probe.nginx-configuration",
133            Self::ApacheConfiguration => "web.probe.apache-configuration",
134            Self::PhpInputDiscovery => "web.probe.php-inputs",
135            Self::LaravelRouteDiscovery => "web.probe.laravel-routes",
136            Self::LaravelInputAnalysis => "web.probe.laravel-inputs",
137            Self::LivewireComponentDiscovery => "web.probe.livewire-components",
138            Self::SanctumAuthBoundary => "web.probe.sanctum-auth",
139            Self::HttpBasicAuthBoundary => "web.probe.http-basic-auth",
140            Self::HttpBearerAuthBoundary => "web.probe.http-bearer-auth",
141        }
142    }
143
144    /// Returns the stable compact name used in derived profile identities.
145    ///
146    /// This catalog owns the slug so execution and verification layers cannot
147    /// drift when new semantic actions are introduced. Existing verification
148    /// rule identifiers depend on the discovery-action values and must remain
149    /// stable.
150    pub const fn slug(self) -> &'static str {
151        match self {
152            Self::NginxConfiguration => "nginx-configuration",
153            Self::ApacheConfiguration => "apache-configuration",
154            Self::PhpInputDiscovery => "php-input-discovery",
155            Self::LaravelRouteDiscovery => "laravel-route",
156            Self::LaravelInputAnalysis => "laravel-input-analysis",
157            Self::LivewireComponentDiscovery => "livewire-component",
158            Self::SanctumAuthBoundary => "sanctum-auth",
159            Self::HttpBasicAuthBoundary => "http-basic-auth",
160            Self::HttpBearerAuthBoundary => "http-bearer-auth",
161        }
162    }
163}
164
165/// Stable semantic action subset with built-in discovery support.
166pub const STANDARD_WEB_DISCOVERY_ACTIONS: [StandardWebActionKind;
167    STANDARD_WEB_DISCOVERY_ACTION_COUNT] = StandardWebActionKind::discovery();
168
169#[cfg(test)]
170mod tests {
171    use std::collections::BTreeSet;
172
173    use super::*;
174
175    #[test]
176    fn discovery_capability_is_a_stable_subset_of_the_catalog() {
177        assert_eq!(
178            STANDARD_WEB_DISCOVERY_ACTIONS.len(),
179            STANDARD_WEB_DISCOVERY_ACTION_COUNT
180        );
181        for discovery in STANDARD_WEB_DISCOVERY_ACTIONS {
182            assert!(StandardWebActionKind::all().contains(&discovery));
183        }
184    }
185
186    #[test]
187    fn every_standard_action_has_a_unique_stable_slug() {
188        let slugs = StandardWebActionKind::all()
189            .into_iter()
190            .map(StandardWebActionKind::slug)
191            .collect::<BTreeSet<_>>();
192
193        assert_eq!(slugs.len(), STANDARD_WEB_ACTION_COUNT);
194        assert!(slugs.iter().all(|slug| !slug.is_empty()));
195        assert_eq!(
196            StandardWebActionKind::LaravelRouteDiscovery.slug(),
197            "laravel-route"
198        );
199        assert_eq!(
200            StandardWebActionKind::HttpBearerAuthBoundary.slug(),
201            "http-bearer-auth"
202        );
203    }
204}