ruso_runtime/contract.rs
1//! Shared types embedded in Ruso bytecode (constant pools and probe specs).
2//!
3//! `ruso-script` parses source into an AST that uses these types for matchers,
4//! bodies, and metadata so compiled output matches what `ruso-runtime` executes.
5
6/// Finding severity, in ascending order of urgency (`Info` is the catch-all
7/// used when a check declares no `severity`).
8#[derive(Debug, Clone, PartialEq)]
9pub enum Severity {
10 /// Low impact.
11 Low,
12 /// Moderate impact.
13 Medium,
14 /// High impact.
15 High,
16 /// Critical impact — typically remote code execution or full compromise.
17 Critical,
18 /// Informational; no direct security impact. The default when unset.
19 Info,
20}
21
22impl Severity {
23 pub fn as_str(&self) -> &'static str {
24 match self {
25 Self::Low => "low",
26 Self::Medium => "medium",
27 Self::High => "high",
28 Self::Critical => "critical",
29 Self::Info => "info",
30 }
31 }
32}
33
34/// HTTP request method for an `http` probe.
35#[derive(Debug, Clone, PartialEq)]
36pub enum HttpMethod {
37 /// `GET`
38 Get,
39 /// `POST`
40 Post,
41 /// `PUT`
42 Put,
43 /// `PATCH`
44 Patch,
45 /// `DELETE`
46 Delete,
47 /// `HEAD`
48 Head,
49 /// `OPTIONS`
50 Options,
51}
52
53/// A structured request body (`data { … }` / `json { … }`): ordered key/value
54/// pairs. Order is preserved so serialization is deterministic.
55#[derive(Debug, Clone, PartialEq)]
56pub struct ObjectBody {
57 /// Key/value pairs in source order.
58 pub pairs: Vec<(String, BodyValue)>,
59}
60
61/// A value inside an [`ObjectBody`] or multipart body.
62#[derive(Debug, Clone, PartialEq)]
63pub enum BodyValue {
64 /// A literal string.
65 String(String),
66 /// A string containing `{{ var }}` placeholders to interpolate at runtime.
67 Interpolation(String),
68 /// A nested object.
69 Object(ObjectBody),
70 /// Hex-encoded raw bytes.
71 Bytes(String),
72 /// A multipart part.
73 Part(InlinePart),
74}
75
76/// One part of a multipart request body.
77#[derive(Debug, Clone, PartialEq)]
78pub struct InlinePart {
79 /// Optional `filename` for a file part.
80 pub filename: Option<String>,
81 /// The part's content.
82 pub body: InlinePartBody,
83}
84
85/// The content of an [`InlinePart`].
86#[derive(Debug, Clone, PartialEq)]
87pub enum InlinePartBody {
88 /// UTF-8 text.
89 Text(String),
90 /// Hex-encoded raw bytes.
91 Bytes(String),
92}
93
94/// A response field selector: which probe (`target`) and which part of its
95/// response (`kind`) a matcher or evidence rule reads.
96#[derive(Debug, Clone, PartialEq)]
97pub struct QualifiedField {
98 /// The probe name the field belongs to (e.g. `home`).
99 pub target: String,
100 /// Which part of the response to read.
101 pub kind: FieldKind,
102}
103
104/// Which part of a probe response a matcher reads.
105#[derive(Debug, Clone, PartialEq)]
106pub enum FieldKind {
107 /// HTTP status code.
108 Status,
109 /// HTTP response body.
110 Body,
111 /// A named HTTP response header.
112 Header(String),
113 /// HTTP round-trip time.
114 ResponseTime,
115 /// HTTP response body size in bytes.
116 ResponseSize,
117 /// Resolver answers (`dns` without `port` / `payload`).
118 Answer,
119 /// Raw probe bytes (tcp / udp / wire dns). Alias: `banner` in scripts.
120 Response,
121 Banner,
122}
123
124/// A single matcher: a response [field](QualifiedField) tested against a
125/// [predicate](MatchPredicate).
126#[derive(Debug, Clone, PartialEq)]
127pub struct QualifiedMatch {
128 /// The response field to read.
129 pub field: QualifiedField,
130 /// The condition the field must satisfy.
131 pub predicate: MatchPredicate,
132}
133
134/// The condition a [`QualifiedField`] is tested against.
135#[derive(Debug, Clone, PartialEq)]
136pub enum MatchPredicate {
137 /// Numeric/string/duration comparison (`==`, `!=`, `<`, …).
138 Compare {
139 /// The comparison operator.
140 op: CmpOp,
141 /// The right-hand value.
142 value: CmpValue,
143 },
144 /// Substring is present.
145 Contains(String),
146 /// Substring is absent.
147 NotContains(String),
148 /// Rust-syntax regular expression matches.
149 Regex(String),
150}
151
152/// Where an `extract` pulls a value from (HTTP only).
153#[derive(Debug, Clone, PartialEq)]
154pub enum ExtractSource {
155 /// From the response body, optionally via a capture-group regex.
156 Body {
157 /// Probe name.
158 target: String,
159 /// Optional regex; capture group 1 (or the whole match) is extracted.
160 regex: Option<String>,
161 },
162 /// From a named response header.
163 Header {
164 /// Probe name.
165 target: String,
166 /// Header name.
167 name: String,
168 },
169}
170
171/// A proof string attached to a finding. Every rule names an explicit source
172/// (`.body`, `.response`, or `.header "<name>"`); an optional `regex` extracts
173/// capture group 1 (or the whole match) from that source instead of taking it
174/// whole.
175#[derive(Debug, Clone, PartialEq)]
176pub enum EvidenceKind {
177 /// HTTP probe response body (`p.body` / `p.body regex '…'`).
178 Body {
179 /// Probe name.
180 target: String,
181 /// Optional regex; `None` takes the whole (truncated) body.
182 pattern: Option<String>,
183 },
184 /// Probe response payload — HTTP body, DNS answers, or socket data
185 /// (`p.response` / `p.response regex '…'`).
186 Response {
187 /// Probe name.
188 target: String,
189 /// Optional regex; `None` takes the whole (truncated) payload.
190 pattern: Option<String>,
191 },
192 /// A named HTTP response header value (`p.header "X" ` / `p.header "X" regex '…'`).
193 Header {
194 /// Probe name.
195 target: String,
196 /// Header name (case-insensitive).
197 name: String,
198 /// Optional regex; `None` takes the whole header value.
199 pattern: Option<String>,
200 },
201}
202
203/// A comparison operator used by [`MatchPredicate::Compare`].
204#[derive(Debug, Clone, Copy, PartialEq)]
205pub enum CmpOp {
206 /// `==`
207 Eq,
208 /// `!=`
209 Ne,
210 /// `<`
211 Lt,
212 /// `>`
213 Gt,
214 /// `<=`
215 Le,
216 /// `>=`
217 Ge,
218}
219
220/// The right-hand operand of a [`MatchPredicate::Compare`].
221#[derive(Debug, Clone, PartialEq)]
222pub enum CmpValue {
223 /// A numeric literal (e.g. a status code or size).
224 Number(u64),
225 /// A string literal.
226 String(String),
227 /// A duration literal (e.g. `500ms`), compared against `response_time`.
228 Duration(String),
229}