Skip to main content

ccboard_core/models/
activity.rs

1//! Activity models for session tool call auditing and security alerting
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7/// A tool call extracted from a session JSONL file
8///
9/// Combines tool_use (from assistant messages) with tool_result (from user messages)
10/// to compute duration and capture output.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ToolCall {
13    /// Tool call ID (from tool_use block)
14    pub id: String,
15
16    /// Session this tool call belongs to
17    pub session_id: String,
18
19    /// Timestamp when the tool was called (from assistant message)
20    pub timestamp: DateTime<Utc>,
21
22    /// Tool name (e.g., "Read", "Bash", "WebFetch", "mcp__server__tool")
23    pub tool_name: String,
24
25    /// Input parameters as JSON
26    pub input: Value,
27
28    /// Duration from call to result in milliseconds (None if no result found)
29    pub duration_ms: Option<u64>,
30
31    /// Tool output content (from tool_result block, truncated preview)
32    pub output: Option<String>,
33}
34
35/// File operation type
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub enum FileOperation {
38    Read,
39    Write,
40    Edit,
41    Glob,
42    Grep,
43}
44
45/// A file access event extracted from tool calls
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct FileAccess {
48    pub session_id: String,
49    pub timestamp: DateTime<Utc>,
50    pub path: String,
51    pub operation: FileOperation,
52    /// Line range for Read operations: (offset, offset + limit)
53    pub line_range: Option<(u64, u64)>,
54}
55
56/// A bash command execution event
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct BashCommand {
59    pub session_id: String,
60    pub timestamp: DateTime<Utc>,
61    pub command: String,
62    pub is_destructive: bool,
63    /// Preview of command output (first 500 chars)
64    pub output_preview: String,
65}
66
67/// Network tool type
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub enum NetworkTool {
70    WebFetch,
71    WebSearch,
72    McpCall { server: String },
73}
74
75/// A network call event
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct NetworkCall {
78    pub session_id: String,
79    pub timestamp: DateTime<Utc>,
80    /// URL for WebFetch, query for WebSearch, empty for MCP
81    pub url: String,
82    pub tool: NetworkTool,
83    /// Extracted domain (empty for WebSearch/MCP without URL)
84    pub domain: String,
85}
86
87/// Alert severity level (ordered: Info < Warning < Critical)
88#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
89pub enum AlertSeverity {
90    Info,
91    Warning,
92    Critical,
93}
94
95/// Alert category for security audit classification
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub enum AlertCategory {
98    CredentialAccess,
99    DestructiveCommand,
100    ExternalExfil,
101    ScopeViolation,
102    ForcePush,
103}
104
105impl AlertCategory {
106    /// Remediation hint shown in the Violations view.
107    /// Match is exhaustive — adding a new variant requires a hint here.
108    pub fn action_hint(&self) -> &'static str {
109        match self {
110            AlertCategory::CredentialAccess => {
111                "Verify the credential wasn't exposed. If it was, rotate it immediately."
112            }
113            AlertCategory::DestructiveCommand => {
114                "Check if deleted files are recoverable (Trash, git stash, backup)."
115            }
116            AlertCategory::ExternalExfil => {
117                "Review what data was sent to this domain and whether it was intentional."
118            }
119            AlertCategory::ScopeViolation => {
120                "Inspect the file written outside the project root. Delete it if unintended."
121            }
122            AlertCategory::ForcePush => {
123                "Run git reflog to find the overwritten commit. Force-push a revert if needed."
124            }
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_action_hint_all_variants_non_empty() {
135        // Exhaustive: if a new AlertCategory variant is added without a hint,
136        // the match in action_hint() will fail to compile.
137        let variants = [
138            AlertCategory::CredentialAccess,
139            AlertCategory::DestructiveCommand,
140            AlertCategory::ExternalExfil,
141            AlertCategory::ScopeViolation,
142            AlertCategory::ForcePush,
143        ];
144        for variant in &variants {
145            let hint = variant.action_hint();
146            assert!(!hint.is_empty(), "{:?} has an empty action hint", variant);
147        }
148    }
149}
150
151/// A security alert generated from activity analysis
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct Alert {
154    pub session_id: String,
155    pub timestamp: DateTime<Utc>,
156    pub severity: AlertSeverity,
157    pub category: AlertCategory,
158    pub detail: String,
159}
160
161/// Summary of all activity extracted from a session
162#[derive(Debug, Clone, Serialize, Deserialize, Default)]
163pub struct ActivitySummary {
164    pub file_accesses: Vec<FileAccess>,
165    pub bash_commands: Vec<BashCommand>,
166    pub network_calls: Vec<NetworkCall>,
167    pub alerts: Vec<Alert>,
168}