fix: harden timeline event input validation and atomic writes
Address security review findings: - Validate event_type against whitelist of 7 known types (M-3) - Validate metadata is valid JSON and under 10KB (M-2, M-4) - Include metadata in audit log details (M-2) - Wrap timeline insert + audit write + timestamp update in a SQLite transaction for atomicity (M-5) - Fix TypeScript TimelineEvent interface: add issue_id, metadata fields and correct created_at type to string (L-3) - Add timeline_events to IssueDetail TypeScript interface (L-4)
This commit is contained in:
parent
13c4969e31
commit
8b0cbc3ce8
@ -533,6 +533,16 @@ pub async fn update_five_why(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VALID_EVENT_TYPES: &[&str] = &[
|
||||||
|
"triage_started",
|
||||||
|
"log_uploaded",
|
||||||
|
"why_level_advanced",
|
||||||
|
"root_cause_identified",
|
||||||
|
"rca_generated",
|
||||||
|
"postmortem_generated",
|
||||||
|
"document_exported",
|
||||||
|
];
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn add_timeline_event(
|
pub async fn add_timeline_event(
|
||||||
issue_id: String,
|
issue_id: String,
|
||||||
@ -541,17 +551,28 @@ pub async fn add_timeline_event(
|
|||||||
metadata: Option<String>,
|
metadata: Option<String>,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<TimelineEvent, String> {
|
) -> Result<TimelineEvent, String> {
|
||||||
|
if !VALID_EVENT_TYPES.contains(&event_type.as_str()) {
|
||||||
|
return Err(format!("Invalid event_type: {event_type}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let meta = metadata.unwrap_or_else(|| "{}".to_string());
|
||||||
|
if meta.len() > 10240 {
|
||||||
|
return Err("metadata exceeds maximum size of 10KB".to_string());
|
||||||
|
}
|
||||||
|
serde_json::from_str::<serde_json::Value>(&meta)
|
||||||
|
.map_err(|_| "metadata must be valid JSON".to_string())?;
|
||||||
|
|
||||||
let event = TimelineEvent::new(
|
let event = TimelineEvent::new(
|
||||||
issue_id.clone(),
|
issue_id.clone(),
|
||||||
event_type.clone(),
|
event_type.clone(),
|
||||||
description.clone(),
|
description.clone(),
|
||||||
metadata.unwrap_or_else(|| "{}".to_string()),
|
meta,
|
||||||
);
|
);
|
||||||
|
|
||||||
let db = state.db.lock().map_err(|e| e.to_string())?;
|
let mut db = state.db.lock().map_err(|e| e.to_string())?;
|
||||||
|
let tx = db.transaction().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// Write to timeline_events table
|
tx.execute(
|
||||||
db.execute(
|
|
||||||
"INSERT INTO timeline_events (id, issue_id, event_type, description, metadata, created_at) \
|
"INSERT INTO timeline_events (id, issue_id, event_type, description, metadata, created_at) \
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||||
rusqlite::params![
|
rusqlite::params![
|
||||||
@ -565,24 +586,24 @@ pub async fn add_timeline_event(
|
|||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// Dual-write to audit_log for security hash chain
|
|
||||||
crate::audit::log::write_audit_event(
|
crate::audit::log::write_audit_event(
|
||||||
&db,
|
&tx,
|
||||||
&event_type,
|
&event_type,
|
||||||
"issue",
|
"issue",
|
||||||
&issue_id,
|
&issue_id,
|
||||||
&serde_json::json!({ "description": description }).to_string(),
|
&serde_json::json!({ "description": description, "metadata": event.metadata }).to_string(),
|
||||||
)
|
)
|
||||||
.map_err(|_| "Failed to write security audit entry".to_string())?;
|
.map_err(|_| "Failed to write security audit entry".to_string())?;
|
||||||
|
|
||||||
// Update issue timestamp
|
|
||||||
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
db.execute(
|
tx.execute(
|
||||||
"UPDATE issues SET updated_at = ?1 WHERE id = ?2",
|
"UPDATE issues SET updated_at = ?1 WHERE id = ?2",
|
||||||
rusqlite::params![now, issue_id],
|
rusqlite::params![now, issue_id],
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
tx.commit().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
Ok(event)
|
Ok(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -74,9 +74,11 @@ export interface FiveWhyEntry {
|
|||||||
|
|
||||||
export interface TimelineEvent {
|
export interface TimelineEvent {
|
||||||
id: string;
|
id: string;
|
||||||
|
issue_id: string;
|
||||||
event_type: string;
|
event_type: string;
|
||||||
description: string;
|
description: string;
|
||||||
created_at: number;
|
metadata: string;
|
||||||
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AiConversation {
|
export interface AiConversation {
|
||||||
@ -104,6 +106,7 @@ export interface IssueDetail {
|
|||||||
image_attachments: ImageAttachment[];
|
image_attachments: ImageAttachment[];
|
||||||
resolution_steps: ResolutionStep[];
|
resolution_steps: ResolutionStep[];
|
||||||
conversations: AiConversation[];
|
conversations: AiConversation[];
|
||||||
|
timeline_events: TimelineEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IssueSummary {
|
export interface IssueSummary {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user