keyhog_scanner/checksum/slack.rs
1use std::sync::LazyLock;
2
3use super::{ChecksumResult, ChecksumValidator};
4
5/// Validates Slack token structure.
6///
7/// Slack tokens do not expose a public checksum algorithm, but their format is
8/// highly regular. This validator performs strict structural matching and
9/// rejects tokens that violate known segment rules.
10pub struct SlackTokenValidator;
11
12// Compile once, reuse across all validate() calls.
13//
14// The bot regex MUST accept both shapes the `slack-bot-token` detector emits
15// (`detectors/slack-bot-token.toml` is the source of truth for what the scanner
16// surfaces, and this validator is the checksum GATE that the emitted match is
17// routed through in `checksum_adjusted_confidence` -> a `ChecksumResult::Invalid`
18// DROPS the finding):
19// * 3-segment canonical: `xoxb-{10-13 digits}-{10-13 digits}-{24-32 alnum}`
20// * 2-segment / "mixed": `xoxb-{10-13 digits}-{15-36 alnum}` (older installs)
21// The second numeric segment is therefore OPTIONAL. A prior `-[0-9]{10,15}-`
22// (mandatory) regex rejected every legitimate 2-segment bot token as Invalid,
23// so the engine silently dropped a real, contract-required ("both must surface")
24// `xoxb-…` finding. Widening the numeric/secret bounds to `{10,15}`/`{15,40}`
25// keeps the wider validator superset of the detector while still anchoring (`$`)
26// and rejecting wrong character classes and too-short/too-long segments.
27static SLACK_BOT_RE: LazyLock<Option<regex::Regex>> = LazyLock::new(|| {
28 regex::Regex::new(r"^xoxb-[0-9]{10,15}(?:-[0-9]{10,15})?-[a-zA-Z0-9]{15,40}$").ok()
29});
30static SLACK_USER_RE: LazyLock<Option<regex::Regex>> = LazyLock::new(|| {
31 regex::Regex::new(r"^xoxp-[0-9]{10,15}-[0-9]{10,15}(?:-[0-9]{10,13})?-[a-zA-Z0-9]{24,40}$").ok()
32});
33
34pub(crate) fn warm_runtime_regexes() {
35 let _ = SLACK_BOT_RE.as_ref();
36 let _ = SLACK_USER_RE.as_ref();
37}
38
39impl SlackTokenValidator {
40 fn is_valid_slack_bot(credential: &str) -> bool {
41 SLACK_BOT_RE
42 .as_ref()
43 .is_some_and(|regex| regex.is_match(credential))
44 }
45
46 fn is_valid_slack_user(credential: &str) -> bool {
47 SLACK_USER_RE
48 .as_ref()
49 .is_some_and(|regex| regex.is_match(credential))
50 }
51}
52
53impl ChecksumValidator for SlackTokenValidator {
54 fn validator_id(&self) -> &str {
55 "slack-token"
56 }
57
58 fn validate(&self, credential: &str) -> ChecksumResult {
59 if credential.starts_with("xoxb-") {
60 if Self::is_valid_slack_bot(credential) {
61 ChecksumResult::Valid
62 } else {
63 ChecksumResult::Invalid
64 }
65 } else if credential.starts_with("xoxp-") {
66 if Self::is_valid_slack_user(credential) {
67 ChecksumResult::Valid
68 } else {
69 ChecksumResult::Invalid
70 }
71 } else {
72 ChecksumResult::NotApplicable
73 }
74 }
75}