#![allow(clippy::expect_used)]
use crate::ops::security_detection::detector_support::{spans, DetectionError};
pub const SPEC_TOML: &str = r#"schema_version = 1
id = "security_detection.detect_hex_run"
archetype = "match-bytes-pattern"
display_name = "Detect Hex Run"
summary = "Returns offsets of hexadecimal byte runs using a config-encoded minimum length."
category = "C"
[intrinsic]
wgsl = "security_detection_detect_hex_run"
[signature]
inputs = ["Bytes", "Bytes"]
output = "Bytes"
laws = []
equivalence_classes = ["long_run", "short_run", "mixed_case", "t47_cap"]
workgroup_size = [64, 1, 1]
tags = ["security-detection", "hex", "secret-scan", "t47"]
fixtures_dir = "fixtures/"
"#;
pub const REFERENCE_VECTORS_TOML: &str = r#"[[case]]
name = "positive_digest"
input = "sha256=0123456789abcdefABCDEF"
min_run_len = 16
expected_offsets = [7]
[[case]]
name = "negative_short_hex"
input = "color #fff is not a secret"
min_run_len = 8
expected_offsets = []
"#;
pub mod lowering {
#[must_use]
pub const fn source() -> &'static str {
r#"struct Params {
input_len: u32,
min_run_len: u32,
max_offsets: u32,
_pad0: u32,
}
struct OffsetOutput {
count: atomic<u32>,
data: array<u32>,
}
@group(0) @binding(0) var<storage, read> input: array<u32>;
@group(0) @binding(1) var<storage, read_write> output: OffsetOutput;
@group(0) @binding(2) var<uniform> params: Params;
fn is_hex(byte: u32) -> bool {
let numeric = byte >= 48u && byte <= 57u;
let upper = byte >= 65u && byte <= 70u;
let lower = byte >= 97u && byte <= 102u;
return numeric || upper || lower;
}
fn is_start(index: u32) -> bool {
if (index >= params.input_len || !is_hex(input[index])) {
return false;
}
if (index == 0u) {
return true;
}
return !is_hex(input[index - 1u]);
}
fn run_end(start: u32) -> u32 {
var end = start;
loop {
if (end >= params.input_len || !is_hex(input[end])) {
break;
}
end = end + 1u;
}
return end;
}
fn emit_offset(offset: u32) {
let slot = atomicAdd(&output.count, 1u);
if (slot < params.max_offsets) {
output.data[slot] = offset;
}
}
@compute @workgroup_size(64)
fn security_detection_detect_hex_run(@builtin(global_invocation_id) gid: vec3<u32>) {
let start = gid.x;
if (params.min_run_len == 0u || !is_start(start)) {
return;
}
let end = run_end(start);
if (end - start >= params.min_run_len) {
emit_offset(start);
}
}
"#
}
}
pub fn detect_hex_run(input: &[u8], min_run_len: u32) -> Result<Vec<u32>, DetectionError> {
spans::hex_run_offsets(input, min_run_len)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DetectHexRunConfig {
pub min_run_len: u32,
}
pub fn detect_hex_run_config(config: &[u8]) -> Result<DetectHexRunConfig, DetectionError> {
let bytes: [u8; 4] = config
.get(..4)
.ok_or_else(|| "Fix: provide at least 4 config bytes for min_run_len".to_string())?
.try_into()
.expect("slice length checked");
Ok(DetectHexRunConfig {
min_run_len: u32::from_le_bytes(bytes),
})
}
pub mod implementation {
pub use super::detect_hex_run;
pub use super::{detect_hex_run_config, DetectHexRunConfig};
pub mod kernel {
pub use super::super::{detect_hex_run, detect_hex_run_config, DetectHexRunConfig};
}
pub mod lowering {
pub mod wgsl {
pub use super::super::super::lowering::source;
}
}
}