ironpost_core/
error.rs

1//! 에러 타입 — 도메인별 에러 정의
2//!
3//! 각 모듈은 자체 에러 타입을 정의하고, `From` 구현을 통해
4//! [`IronpostError`]로 변환합니다.
5
6/// Ironpost 최상위 에러 타입
7///
8/// 모든 도메인 에러를 포함하는 최상위 enum입니다.
9/// 각 모듈의 에러는 `From` 변환을 통해 이 타입으로 변환됩니다.
10#[derive(Debug, thiserror::Error)]
11pub enum IronpostError {
12    /// 설정 관련 에러
13    #[error("config error: {0}")]
14    Config(#[from] ConfigError),
15
16    /// 파이프라인 처리 에러
17    #[error("pipeline error: {0}")]
18    Pipeline(#[from] PipelineError),
19
20    /// 탐지 엔진 에러
21    #[error("detection error: {0}")]
22    Detection(#[from] DetectionError),
23
24    /// 파싱 에러
25    #[error("parse error: {0}")]
26    Parse(#[from] ParseError),
27
28    /// 스토리지 에러
29    #[error("storage error: {0}")]
30    Storage(#[from] StorageError),
31
32    /// 컨테이너 관련 에러
33    #[error("container error: {0}")]
34    Container(#[from] ContainerError),
35
36    /// SBOM 관련 에러
37    #[error("sbom error: {0}")]
38    Sbom(#[from] SbomError),
39
40    /// 플러그인 에러
41    #[error("plugin error: {0}")]
42    Plugin(#[from] PluginError),
43
44    /// I/O 에러
45    #[error("io error: {0}")]
46    Io(#[from] std::io::Error),
47}
48
49/// 설정 관련 에러
50#[derive(Debug, thiserror::Error)]
51pub enum ConfigError {
52    /// 설정 파일을 찾을 수 없음
53    #[error("config file not found: {path}")]
54    FileNotFound {
55        /// 찾으려 했던 파일 경로
56        path: String,
57    },
58
59    /// 설정 파싱 실패
60    #[error("failed to parse config: {reason}")]
61    ParseFailed {
62        /// 파싱 실패 사유
63        reason: String,
64    },
65
66    /// 유효하지 않은 설정 값
67    #[error("invalid config value for '{field}': {reason}")]
68    InvalidValue {
69        /// 설정 필드명
70        field: String,
71        /// 유효하지 않은 사유
72        reason: String,
73    },
74}
75
76/// 파이프라인 처리 에러
77#[derive(Debug, thiserror::Error)]
78pub enum PipelineError {
79    /// 채널 전송 실패
80    #[error("channel send failed: {0}")]
81    ChannelSend(String),
82
83    /// 채널 수신 실패
84    #[error("channel receive failed: {0}")]
85    ChannelRecv(String),
86
87    /// 파이프라인 초기화 실패
88    #[error("pipeline init failed: {0}")]
89    InitFailed(String),
90
91    /// 파이프라인이 이미 실행 중
92    #[error("pipeline already running")]
93    AlreadyRunning,
94
95    /// 파이프라인이 실행 중이 아님
96    #[error("pipeline not running")]
97    NotRunning,
98}
99
100/// 탐지 엔진 에러
101#[derive(Debug, thiserror::Error)]
102pub enum DetectionError {
103    /// eBPF 프로그램 로드 실패
104    #[error("ebpf load failed: {0}")]
105    EbpfLoad(String),
106
107    /// eBPF 맵 접근 실패
108    #[error("ebpf map error: {0}")]
109    EbpfMap(String),
110
111    /// 탐지 규칙 에러
112    #[error("rule error: {0}")]
113    Rule(String),
114}
115
116/// 파싱 에러
117#[derive(Debug, thiserror::Error)]
118pub enum ParseError {
119    /// 지원하지 않는 형식
120    #[error("unsupported format: {0}")]
121    UnsupportedFormat(String),
122
123    /// 파싱 실패
124    #[error("parse failed at offset {offset}: {reason}")]
125    Failed {
126        /// 실패 위치 (바이트 오프셋)
127        offset: usize,
128        /// 실패 사유
129        reason: String,
130    },
131
132    /// 입력 데이터 초과
133    #[error("input too large: {size} bytes (max: {max})")]
134    TooLarge {
135        /// 입력 크기
136        size: usize,
137        /// 최대 허용 크기
138        max: usize,
139    },
140}
141
142/// 스토리지 에러
143#[derive(Debug, thiserror::Error)]
144pub enum StorageError {
145    /// 연결 실패
146    #[error("connection failed: {0}")]
147    Connection(String),
148
149    /// 쿼리 실패
150    #[error("query failed: {0}")]
151    Query(String),
152}
153
154/// 컨테이너 관련 에러
155#[derive(Debug, thiserror::Error)]
156pub enum ContainerError {
157    /// Docker API 호출 실패
158    #[error("docker api error: {0}")]
159    DockerApi(String),
160
161    /// 컨테이너 격리 실패
162    #[error("isolation failed for container '{container_id}': {reason}")]
163    IsolationFailed {
164        /// 대상 컨테이너 ID
165        container_id: String,
166        /// 격리 실패 사유
167        reason: String,
168    },
169
170    /// 정책 위반
171    #[error("policy violation: {0}")]
172    PolicyViolation(String),
173
174    /// 컨테이너를 찾을 수 없음
175    #[error("container not found: {0}")]
176    NotFound(String),
177}
178
179/// SBOM 관련 에러
180#[derive(Debug, thiserror::Error)]
181pub enum SbomError {
182    /// SBOM 스캔 실패
183    #[error("scan failed: {0}")]
184    ScanFailed(String),
185
186    /// 취약점 DB 에러
187    #[error("vulnerability database error: {0}")]
188    VulnDb(String),
189
190    /// 지원하지 않는 SBOM 형식
191    #[error("unsupported sbom format: {0}")]
192    UnsupportedFormat(String),
193
194    /// SBOM 파싱 실패
195    #[error("sbom parse failed: {0}")]
196    ParseFailed(String),
197}
198
199/// 플러그인 에러
200#[derive(Debug, thiserror::Error)]
201pub enum PluginError {
202    /// 이미 등록된 플러그인
203    #[error("plugin already registered: {name}")]
204    AlreadyRegistered {
205        /// 중복된 플러그인 이름
206        name: String,
207    },
208
209    /// 플러그인을 찾을 수 없음
210    #[error("plugin not found: {name}")]
211    NotFound {
212        /// 찾으려 했던 플러그인 이름
213        name: String,
214    },
215
216    /// 잘못된 상태 전환
217    #[error("invalid state for plugin '{name}': current={current}, expected={expected}")]
218    InvalidState {
219        /// 플러그인 이름
220        name: String,
221        /// 현재 상태
222        current: String,
223        /// 기대 상태
224        expected: String,
225    },
226
227    /// 정지 중 에러 발생 (복수 에러)
228    #[error("errors stopping plugins: {0}")]
229    StopFailed(String),
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn config_error_display() {
238        let err = ConfigError::FileNotFound {
239            path: "/etc/ironpost.toml".to_owned(),
240        };
241        assert_eq!(err.to_string(), "config file not found: /etc/ironpost.toml");
242
243        let err = ConfigError::InvalidValue {
244            field: "log_level".to_owned(),
245            reason: "must be one of: trace, debug, info, warn, error".to_owned(),
246        };
247        assert!(err.to_string().contains("log_level"));
248    }
249
250    #[test]
251    fn pipeline_error_display() {
252        let err = PipelineError::AlreadyRunning;
253        assert_eq!(err.to_string(), "pipeline already running");
254
255        let err = PipelineError::ChannelSend("buffer full".to_owned());
256        assert!(err.to_string().contains("buffer full"));
257    }
258
259    #[test]
260    fn detection_error_display() {
261        let err = DetectionError::EbpfLoad("permission denied".to_owned());
262        assert!(err.to_string().contains("permission denied"));
263    }
264
265    #[test]
266    fn parse_error_display() {
267        let err = ParseError::TooLarge {
268            size: 2048,
269            max: 1024,
270        };
271        assert!(err.to_string().contains("2048"));
272        assert!(err.to_string().contains("1024"));
273    }
274
275    #[test]
276    fn container_error_display() {
277        let err = ContainerError::IsolationFailed {
278            container_id: "abc123".to_owned(),
279            reason: "network disconnect failed".to_owned(),
280        };
281        assert!(err.to_string().contains("abc123"));
282        assert!(err.to_string().contains("network disconnect failed"));
283    }
284
285    #[test]
286    fn sbom_error_display() {
287        let err = SbomError::UnsupportedFormat("unknown-format".to_owned());
288        assert!(err.to_string().contains("unknown-format"));
289    }
290
291    #[test]
292    fn ironpost_error_from_config() {
293        let config_err = ConfigError::FileNotFound {
294            path: "test.toml".to_owned(),
295        };
296        let err: IronpostError = config_err.into();
297        assert!(matches!(err, IronpostError::Config(_)));
298        assert!(err.to_string().contains("test.toml"));
299    }
300
301    #[test]
302    fn ironpost_error_from_container() {
303        let container_err = ContainerError::NotFound("xyz".to_owned());
304        let err: IronpostError = container_err.into();
305        assert!(matches!(err, IronpostError::Container(_)));
306    }
307
308    #[test]
309    fn ironpost_error_from_sbom() {
310        let sbom_err = SbomError::ScanFailed("timeout".to_owned());
311        let err: IronpostError = sbom_err.into();
312        assert!(matches!(err, IronpostError::Sbom(_)));
313    }
314
315    #[test]
316    fn ironpost_error_from_io() {
317        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
318        let err: IronpostError = io_err.into();
319        assert!(matches!(err, IronpostError::Io(_)));
320    }
321}