|
1 | 1 | use std::collections::HashMap;
|
2 |
| -use std::fs::{self, FileType}; |
| 2 | +use std::fs::{self, FileType, OpenOptions}; |
| 3 | +use std::io::Write; |
3 | 4 | use std::os::unix::fs::{FileTypeExt, PermissionsExt};
|
4 | 5 |
|
5 | 6 | use async_trait::async_trait;
|
6 | 7 | use chrono::{DateTime, Local};
|
| 8 | +use colored::Colorize; |
7 | 9 | use libc::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR};
|
8 | 10 |
|
9 | 11 | use anyhow::Result;
|
| 12 | +use serde::Serialize; |
10 | 13 |
|
11 | 14 | use super::{Action, Namespace};
|
12 | 15 | use crate::agent::state::SharedState;
|
| 16 | +use crate::agent::task::variables::get_variable; |
13 | 17 |
|
14 | 18 | // cast needed for Darwin apparently
|
15 | 19 | #[allow(clippy::unnecessary_cast)]
|
@@ -154,11 +158,91 @@ impl Action for ReadFile {
|
154 | 158 | }
|
155 | 159 | }
|
156 | 160 |
|
| 161 | +const DEFAULT_APPEND_TO_FILE_TARGET: &str = "findings.jsonl"; |
| 162 | + |
| 163 | +#[derive(Debug, Default, Serialize)] |
| 164 | +struct InvalidJSON { |
| 165 | + data: String, |
| 166 | +} |
| 167 | + |
| 168 | +#[derive(Debug, Default, Clone)] |
| 169 | +struct AppendToFile {} |
| 170 | + |
| 171 | +#[async_trait] |
| 172 | +impl Action for AppendToFile { |
| 173 | + fn name(&self) -> &str { |
| 174 | + "append_to_file" |
| 175 | + } |
| 176 | + |
| 177 | + fn description(&self) -> &str { |
| 178 | + include_str!("append_to_file.prompt") |
| 179 | + } |
| 180 | + |
| 181 | + fn example_payload(&self) -> Option<&str> { |
| 182 | + Some( |
| 183 | + r#"{ |
| 184 | + "title": "Example title", |
| 185 | + "description": "Example description.", |
| 186 | + }"#, |
| 187 | + ) |
| 188 | + } |
| 189 | + |
| 190 | + async fn run( |
| 191 | + &self, |
| 192 | + _: SharedState, |
| 193 | + _: Option<HashMap<String, String>>, |
| 194 | + payload: Option<String>, |
| 195 | + ) -> Result<Option<String>> { |
| 196 | + let payload = payload.unwrap(); |
| 197 | + |
| 198 | + let filepath = match get_variable("filesystem.append_to_file.target") { |
| 199 | + Some(filepath) => filepath, |
| 200 | + None => { |
| 201 | + log::warn!( |
| 202 | + "filesystem.append_to_file.target not defined, using default {}", |
| 203 | + DEFAULT_APPEND_TO_FILE_TARGET |
| 204 | + ); |
| 205 | + DEFAULT_APPEND_TO_FILE_TARGET.to_string() |
| 206 | + } |
| 207 | + }; |
| 208 | + |
| 209 | + // parse the payload as a JSON object |
| 210 | + let one_line_json = if let Ok(value) = serde_json::from_str::<serde_json::Value>(&payload) { |
| 211 | + // reconvert to make sure it's on a single line |
| 212 | + serde_json::to_string(&value).unwrap() |
| 213 | + } else { |
| 214 | + log::error!("can't parse payload as JSON: {}", payload); |
| 215 | + serde_json::to_string(&InvalidJSON { data: payload }).unwrap() |
| 216 | + }; |
| 217 | + |
| 218 | + // append the JSON to the file |
| 219 | + let mut file = OpenOptions::new() |
| 220 | + .append(true) |
| 221 | + .create(true) |
| 222 | + .open(&filepath)?; |
| 223 | + |
| 224 | + writeln!(file, "{}", one_line_json)?; |
| 225 | + |
| 226 | + log::info!( |
| 227 | + "{}: appended {} bytes to {}", |
| 228 | + "filesystem.append_to_file".dimmed(), |
| 229 | + one_line_json.len(), |
| 230 | + filepath.bold() |
| 231 | + ); |
| 232 | + |
| 233 | + Ok(None) |
| 234 | + } |
| 235 | +} |
| 236 | + |
157 | 237 | pub fn get_namespace() -> Namespace {
|
158 | 238 | Namespace::new_non_default(
|
159 | 239 | "Filesystem".to_string(),
|
160 | 240 | include_str!("ns.prompt").to_string(),
|
161 |
| - vec![Box::<ReadFile>::default(), Box::<ReadFolder>::default()], |
| 241 | + vec![ |
| 242 | + Box::<ReadFile>::default(), |
| 243 | + Box::<ReadFolder>::default(), |
| 244 | + Box::<AppendToFile>::default(), |
| 245 | + ], |
162 | 246 | None,
|
163 | 247 | )
|
164 | 248 | }
|
0 commit comments