claude_native/rules/project_specific/
serverless.rs1use crate::detection::{PrimaryType, ProjectType};
2use crate::rules::*;
3use crate::scan::ProjectContext;
4
5fn is_serverless(pt: &ProjectType) -> bool {
6 matches!(pt.primary, PrimaryType::Serverless(_))
7}
8
9pub struct DeployArtifactsIgnored;
12
13impl Rule for DeployArtifactsIgnored {
14 fn id(&self) -> &str { "SLS1" }
15 fn name(&self) -> &str { "Deployment artifacts are ignored" }
16 fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
17 fn severity(&self) -> Severity { Severity::High }
18
19 fn applies_to(&self, pt: &ProjectType) -> bool { is_serverless(pt) }
20
21 fn check(&self, ctx: &ProjectContext) -> RuleResult {
22 let artifacts = [".aws-sam", ".serverless", ".vercel", "cdk.out"];
23 let present: Vec<&&str> = artifacts.iter().filter(|a| ctx.root.join(a).is_dir()).collect();
24
25 if present.is_empty() {
26 return self.pass();
27 }
28
29 let all_ignored = present.iter().all(|a| ctx.claudeignore_contains(a));
30 if all_ignored {
31 self.pass()
32 } else {
33 self.fail(
34 &format!("Deployment artifacts not in .claudeignore: {}", present.iter().map(|a| a.to_string()).collect::<Vec<_>>().join(", ")),
35 Suggestion {
36 priority: SuggestionPriority::QuickWin,
37 title: "Ignore deployment artifacts".into(),
38 description: format!("Add to .claudeignore: {}. sam build generates 10,000+ line CloudFormation templates.", present.iter().map(|a| format!("{a}/")).collect::<Vec<_>>().join(", ")),
39 effort: Effort::Minutes,
40 },
41 )
42 }
43 }
44}
45
46pub struct FunctionsFocused;
49
50impl Rule for FunctionsFocused {
51 fn id(&self) -> &str { "SLS2" }
52 fn name(&self) -> &str { "Function handlers are <100 lines" }
53 fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
54 fn severity(&self) -> Severity { Severity::Medium }
55
56 fn applies_to(&self, pt: &ProjectType) -> bool { is_serverless(pt) }
57
58 fn check(&self, ctx: &ProjectContext) -> RuleResult {
59 let handler_patterns = ["handler", "index", "lambda", "function"];
60 let large_handlers: Vec<_> = ctx.all_files.iter()
61 .filter(|f| {
62 let name = f.path.file_stem().and_then(|n| n.to_str()).unwrap_or("").to_lowercase();
63 handler_patterns.iter().any(|p| name.contains(p)) && f.line_count > 100
64 })
65 .collect();
66
67 if large_handlers.is_empty() {
68 self.pass()
69 } else {
70 let examples: Vec<String> = large_handlers.iter().take(3)
71 .map(|f| format!("{} ({} lines)", f.relative_path.display(), f.line_count))
72 .collect();
73 self.warn(
74 &format!("Large handler files: {}", examples.join(", ")),
75 Suggestion {
76 priority: SuggestionPriority::HighImpact,
77 title: "Keep handlers under 100 lines".into(),
78 description: "Serverless functions should be small by design. Extract business logic into separate modules and keep handlers thin.".into(),
79 effort: Effort::Hour,
80 },
81 )
82 }
83 }
84}
85
86pub struct TestEventsExist;
89
90impl Rule for TestEventsExist {
91 fn id(&self) -> &str { "SLS3" }
92 fn name(&self) -> &str { "Test event files exist" }
93 fn dimension(&self) -> Dimension { Dimension::CodeQuality }
94 fn severity(&self) -> Severity { Severity::Medium }
95
96 fn applies_to(&self, pt: &ProjectType) -> bool { is_serverless(pt) }
97
98 fn check(&self, ctx: &ProjectContext) -> RuleResult {
99 let has_events = ctx.root.join("events").is_dir()
100 || ctx.root.join("test-events").is_dir()
101 || ctx.all_files.iter().any(|f| {
102 f.relative_path.to_string_lossy().contains("event") && f.path.extension().map(|e| e == "json").unwrap_or(false)
103 });
104
105 if has_events {
106 self.pass()
107 } else {
108 self.warn(
109 "No test event files found (events/ or test-events/)",
110 Suggestion {
111 priority: SuggestionPriority::NiceToHave,
112 title: "Add test event files".into(),
113 description: "Create events/ directory with sample event JSON files. Claude needs these to test functions locally with `sam local invoke`.".into(),
114 effort: Effort::Minutes,
115 },
116 )
117 }
118 }
119}
120
121pub struct NoCloudCreds;
124
125impl Rule for NoCloudCreds {
126 fn id(&self) -> &str { "SLS4" }
127 fn name(&self) -> &str { "Cloud credentials not in repo" }
128 fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
129 fn severity(&self) -> Severity { Severity::Critical }
130
131 fn applies_to(&self, pt: &ProjectType) -> bool { is_serverless(pt) }
132
133 fn check(&self, ctx: &ProjectContext) -> RuleResult {
134 let cred_dirs = [".aws", "credentials"];
135 let found: Vec<&&str> = cred_dirs.iter().filter(|d| ctx.root.join(d).exists()).collect();
136
137 if found.is_empty() {
138 self.pass()
139 } else {
140 self.fail(
141 &format!("Cloud credential files/directories found: {}", found.iter().map(|d| d.to_string()).collect::<Vec<_>>().join(", ")),
142 Suggestion {
143 priority: SuggestionPriority::QuickWin,
144 title: "Remove cloud credentials from repo".into(),
145 description: "Serverless projects have direct cloud access. Leaked credentials = full cloud compromise. Add to .gitignore immediately.".into(),
146 effort: Effort::Minutes,
147 },
148 )
149 }
150 }
151}