autoit/format.rs
1//! Format metadata and analysis observations.
2
3use crate::{Encoding, InputKind};
4
5/// Summary of recognized container-level facts.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct ContainerInfo {
8 input_kind: InputKind,
9 encoding: Option<Encoding>,
10 autoit_signature_offset: Option<usize>,
11 version_marker: Option<VersionMarkerInfo>,
12 payload_stream_offsets: Vec<usize>,
13 pe_script_resource: Option<PeScriptResourceInfo>,
14 packed_markers: Vec<PackedMarkerInfo>,
15}
16
17impl ContainerInfo {
18 /// Builds container facts from recorded observations.
19 ///
20 /// Folds the recorded observations into a single summary, capturing the
21 /// signature offset, version marker, payload stream offsets, PE script
22 /// resource, and packed markers. Observations without container facts
23 /// ([`Observation::OuterPeHeaderFound`],
24 /// [`Observation::FirstFileRecordDecrypted`], and
25 /// [`Observation::KnownSubtypeDecrypted`]) are ignored.
26 ///
27 /// # Arguments
28 ///
29 /// * `input_kind` - The recognized outer [`InputKind`].
30 /// * `encoding` - The recognized [`Encoding`], if any.
31 /// * `observations` - Observations to fold into the summary.
32 ///
33 /// # Returns
34 ///
35 /// A populated [`ContainerInfo`].
36 #[must_use]
37 pub fn from_observations(
38 input_kind: InputKind,
39 encoding: Option<Encoding>,
40 observations: &[Observation],
41 ) -> Self {
42 let mut info = Self {
43 input_kind,
44 encoding,
45 autoit_signature_offset: None,
46 version_marker: None,
47 payload_stream_offsets: Vec::new(),
48 pe_script_resource: None,
49 packed_markers: Vec::new(),
50 };
51 for observation in observations {
52 match *observation {
53 Observation::AutoItSignatureFound { offset } => {
54 info.autoit_signature_offset = Some(offset);
55 }
56 Observation::VersionMarkerFound { encoding, offset } => {
57 info.version_marker = Some(VersionMarkerInfo { encoding, offset });
58 }
59 Observation::PayloadStreamStartFound { offset } => {
60 info.payload_stream_offsets.push(offset);
61 }
62 Observation::PeScriptResourceFound {
63 type_id,
64 type_name,
65 name,
66 language_id,
67 rva,
68 offset,
69 size,
70 } => {
71 info.pe_script_resource = Some(PeScriptResourceInfo {
72 type_id,
73 type_name,
74 name,
75 language_id,
76 rva,
77 offset,
78 size,
79 });
80 }
81 Observation::PackedMarkerFound { name, offset } => {
82 info.packed_markers.push(PackedMarkerInfo { name, offset });
83 }
84 Observation::OuterPeHeaderFound
85 | Observation::FirstFileRecordDecrypted { .. }
86 | Observation::KnownSubtypeDecrypted { .. } => {}
87 }
88 }
89 info
90 }
91
92 /// Returns recognized input kind.
93 ///
94 /// # Returns
95 ///
96 /// The [`InputKind`] recorded for this container.
97 #[must_use]
98 pub const fn input_kind(&self) -> InputKind {
99 self.input_kind
100 }
101
102 /// Returns recognized encoding.
103 ///
104 /// # Returns
105 ///
106 /// `Some` with the recognized [`Encoding`], or `None` when none was determined.
107 #[must_use]
108 pub const fn encoding(&self) -> Option<Encoding> {
109 self.encoding
110 }
111
112 /// Returns AutoIt signature offset when present.
113 ///
114 /// # Returns
115 ///
116 /// `Some` with the file offset of the `AU3!` signature, or `None` when no
117 /// signature was located.
118 #[must_use]
119 pub const fn autoit_signature_offset(&self) -> Option<usize> {
120 self.autoit_signature_offset
121 }
122
123 /// Returns version marker facts when present.
124 ///
125 /// # Returns
126 ///
127 /// `Some` with the [`VersionMarkerInfo`], or `None` when no version marker was
128 /// found.
129 #[must_use]
130 pub const fn version_marker(&self) -> Option<VersionMarkerInfo> {
131 self.version_marker
132 }
133
134 /// Returns payload stream start offsets.
135 ///
136 /// # Returns
137 ///
138 /// A slice of file offsets where record streams are expected to begin, in
139 /// recording order.
140 #[must_use]
141 pub fn payload_stream_offsets(&self) -> &[usize] {
142 self.payload_stream_offsets.as_slice()
143 }
144
145 /// Returns PE script resource facts when present.
146 ///
147 /// # Returns
148 ///
149 /// `Some` with the [`PeScriptResourceInfo`], or `None` when no PE script
150 /// resource was found.
151 #[must_use]
152 pub const fn pe_script_resource(&self) -> Option<PeScriptResourceInfo> {
153 self.pe_script_resource
154 }
155
156 /// Returns packed-container markers found in the outer input.
157 ///
158 /// # Returns
159 ///
160 /// A slice of [`PackedMarkerInfo`] entries, in recording order.
161 #[must_use]
162 pub fn packed_markers(&self) -> &[PackedMarkerInfo] {
163 self.packed_markers.as_slice()
164 }
165}
166
167/// Version marker facts.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub struct VersionMarkerInfo {
170 /// Encoding represented by the marker.
171 pub encoding: Encoding,
172 /// File offset of the marker.
173 pub offset: usize,
174}
175
176/// PE `RT_RCDATA/SCRIPT` resource facts.
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub struct PeScriptResourceInfo {
179 /// PE resource type id.
180 pub type_id: u16,
181 /// PE resource type name, when recognized.
182 pub type_name: Option<&'static str>,
183 /// PE resource name.
184 pub name: &'static str,
185 /// PE resource language id, when present.
186 pub language_id: Option<u16>,
187 /// Resource data RVA.
188 pub rva: u32,
189 /// Resource data file offset.
190 pub offset: usize,
191 /// Resource data size.
192 pub size: u32,
193}
194
195/// Packed-container marker facts.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub struct PackedMarkerInfo {
198 /// Marker name.
199 pub name: &'static str,
200 /// File offset where the marker begins.
201 pub offset: usize,
202}
203
204/// Log of analysis observations.
205///
206/// Records concrete observations made while analyzing an AutoIt payload. It
207/// intentionally contains facts rather than behavior labels.
208#[derive(Debug, Clone, Default, PartialEq, Eq)]
209pub struct ObservationLog {
210 entries: Vec<Observation>,
211}
212
213impl ObservationLog {
214 /// Creates an empty log.
215 ///
216 /// # Returns
217 ///
218 /// An [`ObservationLog`] holding no observations.
219 #[must_use]
220 pub const fn new() -> Self {
221 Self {
222 entries: Vec::new(),
223 }
224 }
225
226 /// Adds an observation.
227 ///
228 /// # Arguments
229 ///
230 /// * `observation` - The [`Observation`] to append, preserving recording order.
231 pub fn push(&mut self, observation: Observation) {
232 self.entries.push(observation);
233 }
234
235 /// Returns the recorded observations.
236 ///
237 /// # Returns
238 ///
239 /// A slice of the recorded [`Observation`] values in recording order.
240 #[must_use]
241 pub fn entries(&self) -> &[Observation] {
242 self.entries.as_slice()
243 }
244}
245
246/// A concrete observation made while analyzing an AutoIt payload.
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum Observation {
249 /// The outer input begins with an `MZ` PE header.
250 OuterPeHeaderFound,
251 /// The AutoIt `AU3!` signature prefix was found at the given file offset.
252 AutoItSignatureFound {
253 /// File offset where the AutoIt signature prefix begins.
254 offset: usize,
255 },
256 /// A supported AutoIt version marker was found at the given file offset.
257 VersionMarkerFound {
258 /// Encoding represented by the version marker.
259 encoding: Encoding,
260 /// File offset where the version marker begins.
261 offset: usize,
262 },
263 /// The offset where the AU3 record stream is expected to begin.
264 PayloadStreamStartFound {
265 /// File offset where record parsing should begin.
266 offset: usize,
267 },
268 /// A PE `RT_RCDATA` resource named `SCRIPT` was found.
269 PeScriptResourceFound {
270 /// PE resource type id.
271 type_id: u16,
272 /// PE resource type name, when recognized.
273 type_name: Option<&'static str>,
274 /// PE resource name.
275 name: &'static str,
276 /// PE resource language id, when present.
277 language_id: Option<u16>,
278 /// Resource data RVA.
279 rva: u32,
280 /// Resource data file offset.
281 offset: usize,
282 /// Resource data size in bytes.
283 size: u32,
284 },
285 /// A known packer marker was found in the outer container.
286 PackedMarkerFound {
287 /// Marker name.
288 name: &'static str,
289 /// File offset where the marker begins.
290 offset: usize,
291 },
292 /// The first AU3 record type decrypted to `FILE`.
293 FirstFileRecordDecrypted {
294 /// Encoding profile used for decryption.
295 encoding: Encoding,
296 /// File offset of the encrypted `FILE` marker.
297 offset: usize,
298 },
299 /// The first AU3 record subtype decrypted to a known script subtype.
300 KnownSubtypeDecrypted {
301 /// Encoding profile used for decryption.
302 encoding: Encoding,
303 /// File offset where the source record begins.
304 offset: usize,
305 /// Known subtype that was recovered.
306 subtype: KnownSubtype,
307 },
308}
309
310/// Known AU3 record subtype categories.
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum KnownSubtype {
313 /// Tokenized script subtype `>>>AUTOIT SCRIPT<<<`.
314 TokenizedScript,
315 /// UTF-16 text script subtype `>AUTOIT UNICODE SCRIPT<`.
316 UnicodeScript,
317 /// Plain text script subtype `>AUTOIT SCRIPT<`.
318 PlainScript,
319}