Skip to main content

fastskill_core/validation/
content_safety.rs

1//! Content safety validation (dangerous pattern checks in SKILL.md and scripts).
2
3use crate::core::service::ServiceError;
4use crate::validation::result::{ErrorSeverity, ValidationResult};
5use std::path::Path;
6use tokio::fs;
7
8/// Context for dangerous pattern check (used to build error message).
9pub(crate) enum PatternCheckContext<'a> {
10    SkillFile,
11    ScriptFile(&'a Path),
12}
13
14/// Parameters for a single dangerous-pattern check.
15pub(crate) struct DangerousPatternCheck<'a> {
16    pub content: &'a str,
17    pub patterns: &'a [String],
18    pub field: &'a str,
19    pub context: PatternCheckContext<'a>,
20}
21
22pub(crate) fn add_dangerous_pattern_errors(
23    mut result: ValidationResult,
24    check: DangerousPatternCheck<'_>,
25) -> ValidationResult {
26    let message = |p: &str| match &check.context {
27        PatternCheckContext::SkillFile => {
28            format!("Potentially dangerous pattern found in SKILL.md: {}", p)
29        }
30        PatternCheckContext::ScriptFile(path) => format!(
31            "Potentially dangerous pattern found in script {}: {}",
32            path.display(),
33            p
34        ),
35    };
36    for pattern in check.patterns {
37        if check.content.contains(pattern) {
38            result = result.with_error(check.field, &message(pattern), ErrorSeverity::Critical);
39        }
40    }
41    result
42}
43
44pub(crate) async fn validate_skill_file_content(
45    path: &Path,
46    result: ValidationResult,
47    patterns: &[String],
48) -> Result<ValidationResult, ServiceError> {
49    let content = match fs::read_to_string(path).await {
50        Ok(c) => c,
51        Err(e) => {
52            return Ok(result.with_error(
53                "content",
54                &format!("Cannot read SKILL.md content for safety validation: {}", e),
55                ErrorSeverity::Error,
56            ));
57        }
58    };
59    let result = add_dangerous_pattern_errors(
60        result,
61        DangerousPatternCheck {
62            content: &content,
63            patterns,
64            field: "content",
65            context: PatternCheckContext::SkillFile,
66        },
67    );
68    let result = if content.len() > 50_000 {
69        result.with_warning(
70            "content",
71            "SKILL.md content is very large - consider moving detailed information to reference files",
72        )
73    } else {
74        result
75    };
76    Ok(result)
77}
78
79pub(crate) async fn validate_script_file_content(
80    path: &Path,
81    result: ValidationResult,
82    patterns: &[String],
83) -> Result<ValidationResult, ServiceError> {
84    let content = match fs::read_to_string(path).await {
85        Ok(c) => c,
86        Err(e) => {
87            return Ok(result.with_warning(
88                "script_content",
89                &format!(
90                    "Cannot read script file {} for safety validation: {}",
91                    path.display(),
92                    e
93                ),
94            ));
95        }
96    };
97    let result = add_dangerous_pattern_errors(
98        result,
99        DangerousPatternCheck {
100            content: &content,
101            patterns,
102            field: "script_content",
103            context: PatternCheckContext::ScriptFile(path),
104        },
105    );
106    Ok(result)
107}
108
109pub(crate) fn default_dangerous_patterns() -> Vec<String> {
110    vec![
111        "import os".to_string(),
112        "import subprocess".to_string(),
113        "import sys".to_string(),
114        "exec(".to_string(),
115        "eval(".to_string(),
116        "system(".to_string(),
117        "popen(".to_string(),
118        "rm -rf".to_string(),
119        "sudo".to_string(),
120        "chmod 777".to_string(),
121        "chown".to_string(),
122        "su ".to_string(),
123        "passwd".to_string(),
124    ]
125}