1use serde::{Deserialize, Serialize};
13
14use crate::planner::{RiskScore, VerificationTarget};
15
16pub const NATIVE_WEB_REVIEW_ACTION_COUNT: usize = 10
18 + (2 * cfg!(feature = "template-evaluation-review") as usize)
19 + cfg!(feature = "normalization-resilience") as usize
20 + cfg!(feature = "authorization-review") as usize
21 + cfg!(feature = "openapi-review") as usize
22 + cfg!(feature = "rest-review") as usize
23 + cfg!(feature = "ssrf-oast-review") as usize;
24
25pub const NATIVE_WEB_REVIEW_REQUESTS_PER_CASE: usize = 2;
27
28pub const NATIVE_WEB_REVIEW_ACTIVE_REQUESTS_PER_CASE: usize = 1;
30
31#[cfg(feature = "scanning")]
33pub(crate) const NATIVE_WEB_REVIEW_EVIDENCE_NAMESPACE: &str = "web.review.observation";
34#[cfg(feature = "scanning")]
36pub(crate) const NATIVE_WEB_REVIEW_RESPONSE_MARKER: &str = "response-marker";
37
38#[cfg(feature = "scanning")]
40pub(crate) fn native_web_review_response_marker_predicate() -> termivar_core::KnowledgePredicate {
41 termivar_core::KnowledgePredicate::new(
42 NATIVE_WEB_REVIEW_EVIDENCE_NAMESPACE,
43 NATIVE_WEB_REVIEW_RESPONSE_MARKER,
44 )
45 .expect("the native review marker predicate is a valid static identity")
46}
47
48#[cfg(all(feature = "scanning", feature = "authorization-review"))]
51pub(crate) fn authorization_review_phase_terminal_predicate() -> termivar_core::KnowledgePredicate {
52 termivar_core::KnowledgePredicate::new("web.authorization-review.transport", "phase-terminal")
53 .expect("the authorization terminal predicate is a valid static identity")
54}
55
56#[cfg(all(feature = "scanning", feature = "openapi-review"))]
57pub(crate) fn openapi_review_phase_terminal_predicate() -> termivar_core::KnowledgePredicate {
58 termivar_core::KnowledgePredicate::new("web.openapi-review.transport", "phase-terminal")
59 .expect("the OpenAPI terminal predicate is a valid static identity")
60}
61
62#[cfg(all(feature = "scanning", feature = "rest-review"))]
63pub(crate) fn rest_review_catalog_ready_predicate() -> termivar_core::KnowledgePredicate {
64 termivar_core::KnowledgePredicate::new("web.openapi-review.transport", "rest-catalog-ready")
65 .expect("the REST catalog-ready predicate is a valid static identity")
66}
67
68#[cfg(all(feature = "scanning", feature = "rest-review"))]
69pub(crate) fn rest_review_phase_terminal_predicate() -> termivar_core::KnowledgePredicate {
70 termivar_core::KnowledgePredicate::new("web.rest-review.transport", "phase-terminal")
71 .expect("the REST terminal predicate is a valid static identity")
72}
73
74#[cfg(all(feature = "scanning", feature = "ssrf-oast-review"))]
75pub(crate) fn ssrf_oast_review_candidate_ready_predicate() -> termivar_core::KnowledgePredicate {
76 termivar_core::KnowledgePredicate::new("web.ssrf-oast-review.transport", "candidate-ready")
77 .expect("the SSRF OAST candidate-ready predicate is a valid static identity")
78}
79
80#[cfg(all(feature = "scanning", feature = "ssrf-oast-review"))]
81pub(crate) fn ssrf_oast_review_phase_terminal_predicate() -> termivar_core::KnowledgePredicate {
82 termivar_core::KnowledgePredicate::new("web.ssrf-oast-review.transport", "phase-terminal")
83 .expect("the SSRF OAST terminal predicate is a valid static identity")
84}
85
86const CONTROL_CANDIDATE_LEGS: [NativeWebReviewRequestLeg; NATIVE_WEB_REVIEW_REQUESTS_PER_CASE] = [
87 NativeWebReviewRequestLeg::PassiveControl,
88 NativeWebReviewRequestLeg::ActiveCandidate,
89];
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
93#[serde(rename_all = "snake_case")]
94#[non_exhaustive]
95pub enum NativeWebReviewActionKind {
96 CorsPolicyPair,
98 RedirectReflectionQueryPair,
100 ReflectionContextQueryPair,
102 SqlStructuralQueryPair,
104 SqlStructuralQueryReplayPair,
106 SstiStructuralQueryPair,
108 SstiStructuralQueryReplayPair,
110 #[cfg(feature = "template-evaluation-review")]
112 TemplateJinjaUppercaseQueryPair,
113 #[cfg(feature = "template-evaluation-review")]
115 TemplateJinjaUppercaseQueryReplayPair,
116 XssStructuralQueryPair,
118 XssAttributeBoundaryQueryPair,
120 XssScriptLexicalBoundaryQueryPair,
122 #[cfg(feature = "normalization-resilience")]
124 NormalizationResilienceQueryPair,
125 #[cfg(feature = "authorization-review")]
127 ResourceAuthorizationDifferential,
128 #[cfg(feature = "openapi-review")]
130 OpenApiDocumentReplay,
131 #[cfg(feature = "rest-review")]
133 RestReadOnlyReplay,
134 #[cfg(feature = "ssrf-oast-review")]
136 SsrfOastQueryReview,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
141#[serde(rename_all = "snake_case")]
142#[non_exhaustive]
143pub enum NativeWebReviewDifferentialInput {
144 OriginHeader,
146 SingleQueryParameter,
148 #[cfg(feature = "authorization-review")]
150 AuthorizationHeader,
151 #[cfg(feature = "openapi-review")]
153 ExactRequestReplay,
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159#[non_exhaustive]
160pub enum NativeWebReviewRequestLeg {
161 PassiveControl,
163 ActiveCandidate,
165}
166
167impl NativeWebReviewRequestLeg {
168 pub const fn is_active(self) -> bool {
170 matches!(self, Self::ActiveCandidate)
171 }
172}
173
174impl NativeWebReviewActionKind {
175 pub const fn all() -> [Self; NATIVE_WEB_REVIEW_ACTION_COUNT] {
177 [
178 Self::CorsPolicyPair,
179 Self::RedirectReflectionQueryPair,
180 Self::ReflectionContextQueryPair,
181 Self::SqlStructuralQueryPair,
182 Self::SqlStructuralQueryReplayPair,
183 Self::SstiStructuralQueryPair,
184 Self::SstiStructuralQueryReplayPair,
185 #[cfg(feature = "template-evaluation-review")]
186 Self::TemplateJinjaUppercaseQueryPair,
187 #[cfg(feature = "template-evaluation-review")]
188 Self::TemplateJinjaUppercaseQueryReplayPair,
189 Self::XssStructuralQueryPair,
190 Self::XssAttributeBoundaryQueryPair,
191 Self::XssScriptLexicalBoundaryQueryPair,
192 #[cfg(feature = "normalization-resilience")]
193 Self::NormalizationResilienceQueryPair,
194 #[cfg(feature = "authorization-review")]
195 Self::ResourceAuthorizationDifferential,
196 #[cfg(feature = "openapi-review")]
197 Self::OpenApiDocumentReplay,
198 #[cfg(feature = "rest-review")]
199 Self::RestReadOnlyReplay,
200 #[cfg(feature = "ssrf-oast-review")]
201 Self::SsrfOastQueryReview,
202 ]
203 }
204
205 pub const fn action_id(self) -> &'static str {
207 match self {
208 Self::CorsPolicyPair => "web.review.cors.policy-pair@1",
209 Self::RedirectReflectionQueryPair => "web.review.redirect-reflection.query-pair@1",
210 Self::ReflectionContextQueryPair => "web.review.reflection.context-query-pair@1",
211 Self::SqlStructuralQueryPair => "web.review.sql.structural-query-pair@1",
212 Self::SqlStructuralQueryReplayPair => "web.review.sql.structural-query-replay-pair@1",
213 Self::SstiStructuralQueryPair => "web.review.ssti.structural-query-pair@1",
214 Self::SstiStructuralQueryReplayPair => "web.review.ssti.structural-query-replay-pair@1",
215 #[cfg(feature = "template-evaluation-review")]
216 Self::TemplateJinjaUppercaseQueryPair => {
217 "web.review.template-evaluation.jinja-uppercase-query-pair@1"
218 },
219 #[cfg(feature = "template-evaluation-review")]
220 Self::TemplateJinjaUppercaseQueryReplayPair => {
221 "web.review.template-evaluation.jinja-uppercase-query-replay-pair@1"
222 },
223 Self::XssStructuralQueryPair => "web.review.xss.structural-query-pair@1",
224 Self::XssAttributeBoundaryQueryPair => "web.review.xss.attribute-boundary-query-pair@1",
225 Self::XssScriptLexicalBoundaryQueryPair => {
226 "web.review.xss.script-lexical-boundary-query-pair@1"
227 },
228 #[cfg(feature = "normalization-resilience")]
229 Self::NormalizationResilienceQueryPair => {
230 "web.review.normalization-resilience.query-pair@1"
231 },
232 #[cfg(feature = "authorization-review")]
233 Self::ResourceAuthorizationDifferential => {
234 "web.review.authorization.resource-differential"
235 },
236 #[cfg(feature = "openapi-review")]
237 Self::OpenApiDocumentReplay => "web.review.openapi.document-replay@1",
238 #[cfg(feature = "rest-review")]
239 Self::RestReadOnlyReplay => "web.review.rest.readonly-replay@1",
240 #[cfg(feature = "ssrf-oast-review")]
241 Self::SsrfOastQueryReview => "web.review.ssrf.oast-query@1",
242 }
243 }
244
245 pub const fn executor_id(self) -> &'static str {
249 match self {
250 Self::CorsPolicyPair => "web.review.probe.cors-policy-pair@1",
251 Self::RedirectReflectionQueryPair => {
252 "web.review.probe.redirect-reflection-query-pair@1"
253 },
254 Self::ReflectionContextQueryPair => "web.review.probe.reflection-context-query-pair@1",
255 Self::SqlStructuralQueryPair => "web.review.probe.sql-structural-query-pair@1",
256 Self::SqlStructuralQueryReplayPair => {
257 "web.review.probe.sql-structural-query-replay-pair@1"
258 },
259 Self::SstiStructuralQueryPair => "web.review.probe.ssti-structural-query-pair@1",
260 Self::SstiStructuralQueryReplayPair => {
261 "web.review.probe.ssti-structural-query-replay-pair@1"
262 },
263 #[cfg(feature = "template-evaluation-review")]
264 Self::TemplateJinjaUppercaseQueryPair => {
265 "web.review.probe.template-jinja-uppercase-query-pair@1"
266 },
267 #[cfg(feature = "template-evaluation-review")]
268 Self::TemplateJinjaUppercaseQueryReplayPair => {
269 "web.review.probe.template-jinja-uppercase-query-replay-pair@1"
270 },
271 Self::XssStructuralQueryPair => "web.review.probe.xss-structural-query-pair@1",
272 Self::XssAttributeBoundaryQueryPair => {
273 "web.review.probe.xss-attribute-boundary-query-pair@1"
274 },
275 Self::XssScriptLexicalBoundaryQueryPair => {
276 "web.review.probe.xss-script-lexical-boundary-query-pair@1"
277 },
278 #[cfg(feature = "normalization-resilience")]
279 Self::NormalizationResilienceQueryPair => {
280 "web.review.probe.normalization-resilience-query-pair@1"
281 },
282 #[cfg(feature = "authorization-review")]
283 Self::ResourceAuthorizationDifferential => "http.authorization-resource-review",
284 #[cfg(feature = "openapi-review")]
285 Self::OpenApiDocumentReplay => "http.openapi-review",
286 #[cfg(feature = "rest-review")]
287 Self::RestReadOnlyReplay => "http.rest-review",
288 #[cfg(feature = "ssrf-oast-review")]
289 Self::SsrfOastQueryReview => "http.ssrf-oast-review",
290 }
291 }
292
293 pub const fn slug(self) -> &'static str {
295 match self {
296 Self::CorsPolicyPair => "cors-policy-pair",
297 Self::RedirectReflectionQueryPair => "redirect-reflection-query-pair",
298 Self::ReflectionContextQueryPair => "reflection-context-query-pair",
299 Self::SqlStructuralQueryPair => "sql-structural-query-pair",
300 Self::SqlStructuralQueryReplayPair => "sql-structural-query-replay-pair",
301 Self::SstiStructuralQueryPair => "ssti-structural-query-pair",
302 Self::SstiStructuralQueryReplayPair => "ssti-structural-query-replay-pair",
303 #[cfg(feature = "template-evaluation-review")]
304 Self::TemplateJinjaUppercaseQueryPair => "template-jinja-uppercase-query-pair",
305 #[cfg(feature = "template-evaluation-review")]
306 Self::TemplateJinjaUppercaseQueryReplayPair => {
307 "template-jinja-uppercase-query-replay-pair"
308 },
309 Self::XssStructuralQueryPair => "xss-structural-query-pair",
310 Self::XssAttributeBoundaryQueryPair => "xss-attribute-boundary-query-pair",
311 Self::XssScriptLexicalBoundaryQueryPair => "xss-script-lexical-boundary-query-pair",
312 #[cfg(feature = "normalization-resilience")]
313 Self::NormalizationResilienceQueryPair => "normalization-resilience-query-pair",
314 #[cfg(feature = "authorization-review")]
315 Self::ResourceAuthorizationDifferential => "authorization-resource-differential",
316 #[cfg(feature = "openapi-review")]
317 Self::OpenApiDocumentReplay => "openapi-document-replay",
318 #[cfg(feature = "rest-review")]
319 Self::RestReadOnlyReplay => "rest-readonly-replay",
320 #[cfg(feature = "ssrf-oast-review")]
321 Self::SsrfOastQueryReview => "ssrf-oast-query-review",
322 }
323 }
324
325 pub const fn differential_input(self) -> NativeWebReviewDifferentialInput {
327 match self {
328 Self::CorsPolicyPair => NativeWebReviewDifferentialInput::OriginHeader,
329 Self::RedirectReflectionQueryPair => {
330 NativeWebReviewDifferentialInput::SingleQueryParameter
331 },
332 Self::ReflectionContextQueryPair => {
333 NativeWebReviewDifferentialInput::SingleQueryParameter
334 },
335 Self::SqlStructuralQueryPair
336 | Self::SqlStructuralQueryReplayPair
337 | Self::SstiStructuralQueryPair
338 | Self::SstiStructuralQueryReplayPair
339 | Self::XssStructuralQueryPair
340 | Self::XssAttributeBoundaryQueryPair
341 | Self::XssScriptLexicalBoundaryQueryPair => {
342 NativeWebReviewDifferentialInput::SingleQueryParameter
343 },
344 #[cfg(feature = "template-evaluation-review")]
345 Self::TemplateJinjaUppercaseQueryPair | Self::TemplateJinjaUppercaseQueryReplayPair => {
346 NativeWebReviewDifferentialInput::SingleQueryParameter
347 },
348 #[cfg(feature = "normalization-resilience")]
349 Self::NormalizationResilienceQueryPair => {
350 NativeWebReviewDifferentialInput::SingleQueryParameter
351 },
352 #[cfg(feature = "authorization-review")]
353 Self::ResourceAuthorizationDifferential => {
354 NativeWebReviewDifferentialInput::AuthorizationHeader
355 },
356 #[cfg(feature = "openapi-review")]
357 Self::OpenApiDocumentReplay => NativeWebReviewDifferentialInput::ExactRequestReplay,
358 #[cfg(feature = "rest-review")]
359 Self::RestReadOnlyReplay => NativeWebReviewDifferentialInput::ExactRequestReplay,
360 #[cfg(feature = "ssrf-oast-review")]
361 Self::SsrfOastQueryReview => NativeWebReviewDifferentialInput::SingleQueryParameter,
362 }
363 }
364
365 pub const fn request_legs(
370 self,
371 ) -> &'static [NativeWebReviewRequestLeg; NATIVE_WEB_REVIEW_REQUESTS_PER_CASE] {
372 let _ = self;
373 &CONTROL_CANDIDATE_LEGS
374 }
375
376 pub const fn maximum_requests_per_case(self) -> usize {
378 #[cfg(feature = "authorization-review")]
379 if matches!(self, Self::ResourceAuthorizationDifferential) {
380 return 4;
381 }
382 #[cfg(feature = "ssrf-oast-review")]
383 if matches!(self, Self::SsrfOastQueryReview) {
384 return 3;
385 }
386 self.request_legs().len()
387 }
388
389 pub const fn maximum_active_requests_per_case(self) -> usize {
391 let _ = self;
392 NATIVE_WEB_REVIEW_ACTIVE_REQUESTS_PER_CASE
393 }
394
395 pub fn risk(self) -> RiskScore {
399 let percent = match self {
400 Self::CorsPolicyPair => 5,
401 Self::RedirectReflectionQueryPair => 8,
402 Self::ReflectionContextQueryPair => 5,
403 Self::SqlStructuralQueryPair
404 | Self::SqlStructuralQueryReplayPair
405 | Self::SstiStructuralQueryPair
406 | Self::SstiStructuralQueryReplayPair
407 | Self::XssStructuralQueryPair
408 | Self::XssAttributeBoundaryQueryPair
409 | Self::XssScriptLexicalBoundaryQueryPair => 7,
410 #[cfg(feature = "template-evaluation-review")]
411 Self::TemplateJinjaUppercaseQueryPair | Self::TemplateJinjaUppercaseQueryReplayPair => {
412 7
413 },
414 #[cfg(feature = "normalization-resilience")]
415 Self::NormalizationResilienceQueryPair => 7,
416 #[cfg(feature = "authorization-review")]
417 Self::ResourceAuthorizationDifferential => 6,
418 #[cfg(feature = "openapi-review")]
419 Self::OpenApiDocumentReplay => 2,
420 #[cfg(feature = "rest-review")]
421 Self::RestReadOnlyReplay => 2,
422 #[cfg(feature = "ssrf-oast-review")]
423 Self::SsrfOastQueryReview => 5,
424 };
425 RiskScore::from_percent(percent).expect("native web-review risk is a valid constant")
426 }
427
428 pub const fn verification_target(self) -> VerificationTarget {
434 let _ = self;
435 VerificationTarget::KnowledgeOnly
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use std::collections::BTreeSet;
442
443 use crate::{
444 planner::VerificationTarget, web_actions::StandardWebActionKind,
445 web_planning::StandardWebAttackProfile,
446 };
447
448 use super::{
449 NativeWebReviewActionKind, NativeWebReviewDifferentialInput, NativeWebReviewRequestLeg,
450 NATIVE_WEB_REVIEW_ACTION_COUNT, NATIVE_WEB_REVIEW_ACTIVE_REQUESTS_PER_CASE,
451 NATIVE_WEB_REVIEW_REQUESTS_PER_CASE,
452 };
453
454 #[test]
455 fn native_action_identities_are_stable_unique_and_deterministic() {
456 let expected = [
457 (
458 NativeWebReviewActionKind::CorsPolicyPair,
459 "web.review.cors.policy-pair@1",
460 "web.review.probe.cors-policy-pair@1",
461 "cors-policy-pair",
462 ),
463 (
464 NativeWebReviewActionKind::RedirectReflectionQueryPair,
465 "web.review.redirect-reflection.query-pair@1",
466 "web.review.probe.redirect-reflection-query-pair@1",
467 "redirect-reflection-query-pair",
468 ),
469 (
470 NativeWebReviewActionKind::ReflectionContextQueryPair,
471 "web.review.reflection.context-query-pair@1",
472 "web.review.probe.reflection-context-query-pair@1",
473 "reflection-context-query-pair",
474 ),
475 (
476 NativeWebReviewActionKind::SqlStructuralQueryPair,
477 "web.review.sql.structural-query-pair@1",
478 "web.review.probe.sql-structural-query-pair@1",
479 "sql-structural-query-pair",
480 ),
481 (
482 NativeWebReviewActionKind::SqlStructuralQueryReplayPair,
483 "web.review.sql.structural-query-replay-pair@1",
484 "web.review.probe.sql-structural-query-replay-pair@1",
485 "sql-structural-query-replay-pair",
486 ),
487 (
488 NativeWebReviewActionKind::SstiStructuralQueryPair,
489 "web.review.ssti.structural-query-pair@1",
490 "web.review.probe.ssti-structural-query-pair@1",
491 "ssti-structural-query-pair",
492 ),
493 (
494 NativeWebReviewActionKind::SstiStructuralQueryReplayPair,
495 "web.review.ssti.structural-query-replay-pair@1",
496 "web.review.probe.ssti-structural-query-replay-pair@1",
497 "ssti-structural-query-replay-pair",
498 ),
499 #[cfg(feature = "template-evaluation-review")]
500 (
501 NativeWebReviewActionKind::TemplateJinjaUppercaseQueryPair,
502 "web.review.template-evaluation.jinja-uppercase-query-pair@1",
503 "web.review.probe.template-jinja-uppercase-query-pair@1",
504 "template-jinja-uppercase-query-pair",
505 ),
506 #[cfg(feature = "template-evaluation-review")]
507 (
508 NativeWebReviewActionKind::TemplateJinjaUppercaseQueryReplayPair,
509 "web.review.template-evaluation.jinja-uppercase-query-replay-pair@1",
510 "web.review.probe.template-jinja-uppercase-query-replay-pair@1",
511 "template-jinja-uppercase-query-replay-pair",
512 ),
513 (
514 NativeWebReviewActionKind::XssStructuralQueryPair,
515 "web.review.xss.structural-query-pair@1",
516 "web.review.probe.xss-structural-query-pair@1",
517 "xss-structural-query-pair",
518 ),
519 (
520 NativeWebReviewActionKind::XssAttributeBoundaryQueryPair,
521 "web.review.xss.attribute-boundary-query-pair@1",
522 "web.review.probe.xss-attribute-boundary-query-pair@1",
523 "xss-attribute-boundary-query-pair",
524 ),
525 (
526 NativeWebReviewActionKind::XssScriptLexicalBoundaryQueryPair,
527 "web.review.xss.script-lexical-boundary-query-pair@1",
528 "web.review.probe.xss-script-lexical-boundary-query-pair@1",
529 "xss-script-lexical-boundary-query-pair",
530 ),
531 #[cfg(feature = "normalization-resilience")]
532 (
533 NativeWebReviewActionKind::NormalizationResilienceQueryPair,
534 "web.review.normalization-resilience.query-pair@1",
535 "web.review.probe.normalization-resilience-query-pair@1",
536 "normalization-resilience-query-pair",
537 ),
538 #[cfg(feature = "authorization-review")]
539 (
540 NativeWebReviewActionKind::ResourceAuthorizationDifferential,
541 "web.review.authorization.resource-differential",
542 "http.authorization-resource-review",
543 "authorization-resource-differential",
544 ),
545 #[cfg(feature = "openapi-review")]
546 (
547 NativeWebReviewActionKind::OpenApiDocumentReplay,
548 "web.review.openapi.document-replay@1",
549 "http.openapi-review",
550 "openapi-document-replay",
551 ),
552 #[cfg(feature = "rest-review")]
553 (
554 NativeWebReviewActionKind::RestReadOnlyReplay,
555 "web.review.rest.readonly-replay@1",
556 "http.rest-review",
557 "rest-readonly-replay",
558 ),
559 #[cfg(feature = "ssrf-oast-review")]
560 (
561 NativeWebReviewActionKind::SsrfOastQueryReview,
562 "web.review.ssrf.oast-query@1",
563 "http.ssrf-oast-review",
564 "ssrf-oast-query-review",
565 ),
566 ];
567
568 assert_eq!(
569 NativeWebReviewActionKind::all().len(),
570 NATIVE_WEB_REVIEW_ACTION_COUNT
571 );
572 assert_eq!(
573 NativeWebReviewActionKind::all(),
574 expected.map(|entry| entry.0)
575 );
576
577 let mut action_ids = BTreeSet::new();
578 let mut executor_ids = BTreeSet::new();
579 let mut slugs = BTreeSet::new();
580 for (kind, action_id, executor_id, slug) in expected {
581 assert_eq!(kind.action_id(), action_id);
582 assert_eq!(kind.executor_id(), executor_id);
583 assert_eq!(kind.slug(), slug);
584 assert!(action_ids.insert(kind.action_id()));
585 assert!(executor_ids.insert(kind.executor_id()));
586 assert!(slugs.insert(kind.slug()));
587 }
588 }
589
590 #[test]
591 fn every_native_action_is_low_risk_and_irrevocably_knowledge_only() {
592 let expected_risk_basis_points = [
593 500,
594 800,
595 500,
596 700,
597 700,
598 #[cfg(feature = "template-evaluation-review")]
599 700,
600 #[cfg(feature = "template-evaluation-review")]
601 700,
602 700,
603 700,
604 700,
605 700,
606 700,
607 #[cfg(feature = "normalization-resilience")]
608 700,
609 #[cfg(feature = "authorization-review")]
610 600,
611 #[cfg(feature = "openapi-review")]
612 200,
613 #[cfg(feature = "rest-review")]
614 200,
615 ];
616
617 for (kind, expected_risk) in NativeWebReviewActionKind::all()
618 .into_iter()
619 .zip(expected_risk_basis_points)
620 {
621 assert_eq!(kind.risk().basis_points(), expected_risk);
622 assert!(kind.risk().basis_points() <= 1_000);
623 assert_eq!(
624 kind.verification_target(),
625 VerificationTarget::KnowledgeOnly
626 );
627 }
628 }
629
630 #[test]
631 fn request_relationship_is_exactly_passive_control_then_active_candidate() {
632 let expected_legs = [
633 NativeWebReviewRequestLeg::PassiveControl,
634 NativeWebReviewRequestLeg::ActiveCandidate,
635 ];
636
637 for kind in NativeWebReviewActionKind::all() {
638 assert_eq!(kind.request_legs(), &expected_legs);
639 let extended_request_plan = false;
640 #[cfg(feature = "authorization-review")]
641 let extended_request_plan = extended_request_plan
642 || kind == NativeWebReviewActionKind::ResourceAuthorizationDifferential;
643 #[cfg(feature = "authorization-review")]
644 if kind == NativeWebReviewActionKind::ResourceAuthorizationDifferential {
645 assert_eq!(kind.maximum_requests_per_case(), 4);
646 }
647 #[cfg(feature = "ssrf-oast-review")]
648 let extended_request_plan =
649 extended_request_plan || kind == NativeWebReviewActionKind::SsrfOastQueryReview;
650 #[cfg(feature = "ssrf-oast-review")]
651 if kind == NativeWebReviewActionKind::SsrfOastQueryReview {
652 assert_eq!(kind.maximum_requests_per_case(), 3);
653 }
654 if !extended_request_plan {
655 assert_eq!(
656 kind.maximum_requests_per_case(),
657 NATIVE_WEB_REVIEW_REQUESTS_PER_CASE
658 );
659 }
660 assert_eq!(
661 kind.maximum_active_requests_per_case(),
662 NATIVE_WEB_REVIEW_ACTIVE_REQUESTS_PER_CASE
663 );
664 assert_eq!(
665 kind.request_legs()
666 .iter()
667 .filter(|leg| leg.is_active())
668 .count(),
669 1
670 );
671 }
672
673 assert_eq!(
674 NativeWebReviewActionKind::CorsPolicyPair.differential_input(),
675 NativeWebReviewDifferentialInput::OriginHeader
676 );
677 assert_eq!(
678 NativeWebReviewActionKind::RedirectReflectionQueryPair.differential_input(),
679 NativeWebReviewDifferentialInput::SingleQueryParameter
680 );
681 assert_eq!(
682 NativeWebReviewActionKind::ReflectionContextQueryPair.differential_input(),
683 NativeWebReviewDifferentialInput::SingleQueryParameter
684 );
685 }
686
687 #[test]
688 fn native_review_actions_are_absent_from_the_default_standard_catalog() {
689 let native_ids = NativeWebReviewActionKind::all()
690 .into_iter()
691 .map(NativeWebReviewActionKind::action_id)
692 .collect::<BTreeSet<_>>();
693 let standard_ids = StandardWebActionKind::all()
694 .into_iter()
695 .map(StandardWebActionKind::action_id)
696 .collect::<BTreeSet<_>>();
697
698 assert!(native_ids.is_disjoint(&standard_ids));
699
700 let standard_profile = StandardWebAttackProfile::new().unwrap();
701 assert!(standard_profile
702 .actions()
703 .iter()
704 .all(|action| !native_ids.contains(action.id())));
705 }
706}