wowlab_tidy/languages/rust/rules/safety/
sensitive_debug.rs1use ra_ap_syntax::{
2 AstNode,
3 ast::{self, HasAttrs, HasName},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10 Example {
11 label: "Debug on struct with password field",
12 code: "#[derive(Debug)]\nstruct Creds { password: String }",
13 pass: false,
14 },
15 Example {
16 label: "Debug on struct without sensitive fields",
17 code: "#[derive(Debug)]\nstruct User { name: String }",
18 pass: true,
19 },
20 Example {
21 label: "no Debug with password field",
22 code: "struct Creds { password: String }",
23 pass: true,
24 },
25 Example {
26 label: "Debug on struct with token field",
27 code: "#[derive(Debug)]\nstruct Auth { token: String }",
28 pass: false,
29 },
30 Example {
31 label: "Debug on struct with api_key field",
32 code: "#[derive(Debug)]\nstruct Config { api_key: String }",
33 pass: false,
34 },
35 Example {
36 label: "sensitive struct in test module",
37 code: "#[cfg(test)]\nmod tests {\n #[derive(Debug)]\n struct Creds { password: String }\n}",
38 pass: true,
39 },
40];
41
42crate::ast_rule!(
43 sensitive_debug,
44 "Flag `#[derive(Debug)]` on structs with sensitive fields like `password`.",
45 "Deriving Debug on types with passwords or tokens risks leaking secrets in logs and error messages.",
46 High,
47);
48
49const SENSITIVE_FIELDS: &[&str] = &[
50 "password",
51 "secret",
52 "token",
53 "api_key",
54 "private_key",
55 "secret_key",
56 "auth_token",
57 "credential",
58 "credentials",
59 "access_token",
60 "refresh_token",
61];
62
63fn check_sensitive_debug(ctx: &AstCtx<'_>) -> Vec<Violation> {
64 ctx.nodes::<ast::Struct>()
65 .filter(|item| !ctx.is_in_test(item) && has_debug_derive(item))
66 .filter_map(|item| {
67 let sensitive = find_sensitive_fields(&item);
68 let name = item.name()?;
69
70 (!sensitive.is_empty()).then(|| {
71 ctx.violation(
72 &name,
73 format!(
74 "#[derive(Debug)] on struct with sensitive field(s): {} — implement Debug manually to redact",
75 sensitive.join(", ")
76 ),
77 )
78 })
79 })
80 .collect()
81}
82
83fn is_sensitive_field(name: &str) -> bool {
84 SENSITIVE_FIELDS.contains(&name)
85}
86
87fn has_debug_derive(item: &ast::Struct) -> bool {
88 item.attrs().any(|attr| {
89 attr.as_simple_call().is_some_and(|(name, tokens)| {
90 name == "derive"
91 && tokens
92 .syntax()
93 .text()
94 .to_string()
95 .strip_prefix('(')
96 .and_then(|text| text.strip_suffix(')'))
97 .is_some_and(|entries| entries.split(',').any(|entry| entry.trim() == "Debug"))
98 })
99 })
100}
101
102fn find_sensitive_fields(item: &ast::Struct) -> Vec<String> {
103 let Some(ast::FieldList::RecordFieldList(fields)) = item.field_list() else {
104 return Vec::new();
105 };
106
107 let names = fields
108 .fields()
109 .filter_map(|field| field.name())
110 .map(|name| name.text().to_string());
111
112 names.filter(|name| is_sensitive_field(name)).collect()
113}
114
115crate::tidy_ast_test!(check_sensitive_debug, {
116 crate::example_tests!(EXAMPLES, check_sensitive_debug);
117});