Skip to main content

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}