upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/audit.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 06:17:55 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 06:17:55 +0000
commit001ca45e385c05b0eaa36d9879e051853aaff107 (patch)
tree603fb85d2563db5b7c418e9fd143d479bd09676e /grasp-audit/src/audit.rs
parentd428baf30feec295870fadda2d335d1e7f89507b (diff)
created POC grasp-auditor
Diffstat (limited to 'grasp-audit/src/audit.rs')
-rw-r--r--grasp-audit/src/audit.rs188
1 files changed, 188 insertions, 0 deletions
diff --git a/grasp-audit/src/audit.rs b/grasp-audit/src/audit.rs
new file mode 100644
index 0000000..0ca8737
--- /dev/null
+++ b/grasp-audit/src/audit.rs
@@ -0,0 +1,188 @@
1//! Audit configuration and event tagging
2
3use nostr_sdk::prelude::*;
4use std::time::Duration;
5
6/// Audit configuration
7#[derive(Debug, Clone)]
8pub struct AuditConfig {
9 /// Unique ID for this audit run
10 pub run_id: String,
11
12 /// Mode: CI (isolated) or Production (live)
13 pub mode: AuditMode,
14
15 /// Cleanup timestamp (events can be cleaned after this)
16 pub cleanup_after: Timestamp,
17
18 /// Whether to actually create events or just query
19 pub read_only: bool,
20}
21
22/// Audit mode
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum AuditMode {
25 /// Isolated CI/CD tests - only see own events
26 CI,
27
28 /// Production audit - see all events, minimal writes
29 Production,
30}
31
32impl AuditConfig {
33 /// Create config for CI/CD testing
34 pub fn ci() -> Self {
35 let run_id = format!("ci-{}", uuid::Uuid::new_v4());
36 Self {
37 run_id,
38 mode: AuditMode::CI,
39 cleanup_after: Timestamp::now() + 3600, // 1 hour from now
40 read_only: false,
41 }
42 }
43
44 /// Create config for production audit
45 pub fn production() -> Self {
46 let run_id = format!("prod-audit-{}", Timestamp::now().as_u64());
47 Self {
48 run_id,
49 mode: AuditMode::Production,
50 cleanup_after: Timestamp::now() + 300, // 5 minutes from now
51 read_only: true, // Default to read-only for production
52 }
53 }
54
55 /// Create config with custom run ID
56 pub fn with_run_id(run_id: String, mode: AuditMode) -> Self {
57 Self {
58 run_id,
59 mode,
60 cleanup_after: Timestamp::now() + 3600,
61 read_only: mode == AuditMode::Production,
62 }
63 }
64
65 /// Get audit tags for an event
66 pub fn audit_tags(&self) -> Vec<Tag> {
67 vec![
68 Tag::custom(
69 TagKind::Custom(std::borrow::Cow::Borrowed("grasp-audit")),
70 vec!["true"]
71 ),
72 Tag::custom(
73 TagKind::Custom(std::borrow::Cow::Borrowed("audit-run-id")),
74 vec![self.run_id.clone()]
75 ),
76 Tag::custom(
77 TagKind::Custom(std::borrow::Cow::Borrowed("audit-cleanup")),
78 vec![self.cleanup_after.to_string()]
79 ),
80 ]
81 }
82}
83
84/// Builder for audit events
85pub struct AuditEventBuilder {
86 kind: Kind,
87 content: String,
88 tags: Vec<Tag>,
89 config: AuditConfig,
90}
91
92impl AuditEventBuilder {
93 /// Create a new audit event builder
94 pub fn new(kind: Kind, content: impl Into<String>, config: AuditConfig) -> Self {
95 Self {
96 kind,
97 content: content.into(),
98 tags: Vec::new(),
99 config,
100 }
101 }
102
103 /// Add a tag
104 pub fn tag(mut self, tag: Tag) -> Self {
105 self.tags.push(tag);
106 self
107 }
108
109 /// Add multiple tags
110 pub fn tags(mut self, tags: Vec<Tag>) -> Self {
111 self.tags.extend(tags);
112 self
113 }
114
115 /// Build the event with audit tags
116 pub async fn build(self, keys: &Keys) -> anyhow::Result<Event> {
117 let mut all_tags = self.tags;
118 all_tags.extend(self.config.audit_tags());
119
120 let event = EventBuilder::new(self.kind, self.content, all_tags)
121 .to_event(keys)
122 .await?;
123
124 Ok(event)
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_ci_config() {
134 let config = AuditConfig::ci();
135 assert_eq!(config.mode, AuditMode::CI);
136 assert!(!config.read_only);
137 assert!(config.run_id.starts_with("ci-"));
138 }
139
140 #[test]
141 fn test_production_config() {
142 let config = AuditConfig::production();
143 assert_eq!(config.mode, AuditMode::Production);
144 assert!(config.read_only);
145 assert!(config.run_id.starts_with("prod-audit-"));
146 }
147
148 #[test]
149 fn test_audit_tags() {
150 let config = AuditConfig::ci();
151 let tags = config.audit_tags();
152
153 assert_eq!(tags.len(), 3);
154
155 // Check grasp-audit tag
156 assert!(tags.iter().any(|t| {
157 matches!(t.kind(), TagKind::Custom(k) if k == "grasp-audit")
158 }));
159
160 // Check audit-run-id tag
161 assert!(tags.iter().any(|t| {
162 matches!(t.kind(), TagKind::Custom(k) if k == "audit-run-id")
163 }));
164
165 // Check audit-cleanup tag
166 assert!(tags.iter().any(|t| {
167 matches!(t.kind(), TagKind::Custom(k) if k == "audit-cleanup")
168 }));
169 }
170
171 #[tokio::test]
172 async fn test_audit_event_builder() {
173 let config = AuditConfig::ci();
174 let keys = Keys::generate();
175
176 let event = AuditEventBuilder::new(Kind::TextNote, "test", config.clone())
177 .tag(Tag::custom(TagKind::Custom("test".into()), vec!["value"]))
178 .build(&keys)
179 .await
180 .unwrap();
181
182 // Should have our custom tag + 3 audit tags
183 assert!(event.tags.len() >= 4);
184
185 // Verify event is valid
186 assert!(event.verify().is_ok());
187 }
188}