diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e664e67 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: make release + - name: Run tests + run: make test-all + - name: Generate doc + run: make test-doc + - name: Clippy + run: make lint + - name: Generate doc + run: make generate-doc diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a20fc0d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "l9event" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "*", features = ["derive"] } +serde_json = "*" +chrono = { version = "*", features = ["serde"] } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d3a99d9 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +# Clean the project +clean: + cargo clean + +# Build the project +build: + cargo build --all-targets --all-features + +# Build the project in release mode +release: + cargo build --release --all-targets --all-features + +# Test the project's docs comments +test-doc: + cargo test --all-features --release --doc + +# Test the project with all tests and using native cargo test runner +test-all: + cargo test --all-features --release $(CARGO_EXTRA_ARGS) -- --nocapture $(BIN_EXTRA_ARGS) + +# Format the code +format: + cargo +nightly fmt -- --check + +# Lint the code +lint: + cargo clippy --all-features --all-targets --tests $(CARGO_EXTRA_ARGS) -- -W clippy::all -D warnings + +generate-doc: + @echo "" + @echo "Generating the documentation." + @echo "" + RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps + @echo "" + @echo "The documentation is available at: ./target/doc" + @echo "" + +.PHONY: all clean build release test-doc test-all format lint generate-doc diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6001d14 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,157 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +pub const SEVERITY_CRITICAL: &str = "critical"; +pub const SEVERITY_HIGH: &str = "high"; +pub const SEVERITY_MEDIUM: &str = "medium"; +pub const SEVERITY_LOW: &str = "low"; +pub const SEVERITY_INFO: &str = "info"; + +pub const STAGE_OPEN: &str = "open"; +pub const STAGE_EXPLORE: &str = "explore"; +pub const STAGE_EXFILTRATE: &str = "exfiltrate"; + +#[derive(Debug, Serialize, Deserialize)] +pub struct L9Event { + pub event_type: String, + pub event_source: String, + pub event_pipeline: Vec, + pub event_fingerprint: String, + pub ip: String, + pub host: String, + pub reverse: String, + pub port: String, + pub mac: String, + pub vendor: String, + #[serde(rename = "transport")] + pub transports: Vec, + pub protocol: String, + pub http: L9HttpEvent, + pub summary: String, + pub time: DateTime, + pub ssl: L9SSLEvent, + pub ssh: L9SSHEvent, + pub service: L9ServiceEvent, + pub leak: L9LeakEvent, + pub tags: Vec, + pub geoip: GeoLocation, + pub network: Network, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct L9HttpEvent { + pub root: String, + pub url: String, + pub status: i32, + pub length: i64, + #[serde(rename = "header")] + pub headers: HashMap, + pub title: String, + pub favicon_hash: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct L9ServiceEvent { + pub credentials: ServiceCredentials, + pub software: Software, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct L9SSHEvent { + pub fingerprint: String, + pub version: i32, + pub banner: String, + pub motd: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct L9LeakEvent { + pub stage: String, + #[serde(rename = "type")] + pub event_type: String, + pub severity: String, + pub dataset: DatasetSummary, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct L9SSLEvent { + pub detected: bool, + pub enabled: bool, + pub jarm: String, + pub cypher_suite: String, + pub version: String, + pub certificate: Certificate, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DatasetSummary { + pub rows: i64, + pub files: i64, + pub size: i64, + pub collections: i64, + pub infected: bool, + pub ransom_notes: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Software { + pub name: String, + pub version: String, + pub operating_system: Option, + pub modules: Vec, + pub fingerprint: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SoftwareModule { + pub name: String, + pub version: String, + pub fingerprint: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ServiceCredentials { + pub noauth: bool, + pub username: String, + pub password: String, + pub key: String, + pub raw: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Certificate { + pub cn: String, + pub domain: Vec, + pub fingerprint: String, + pub key_algo: String, + pub key_size: i32, + pub issuer_name: String, + pub not_before: DateTime, + pub not_after: DateTime, + pub valid: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GeoLocation { + pub continent_name: Option, + pub region_iso_code: Option, + pub city_name: Option, + pub country_iso_code: Option, + pub country_name: Option, + pub region_name: Option, + pub location: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GeoPoint { + pub lat: f64, + pub lon: f64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Network { + pub organization_name: String, + pub asn: i32, + pub network: String, +} diff --git a/tests/data/sample_event.json b/tests/data/sample_event.json new file mode 100644 index 0000000..651f689 --- /dev/null +++ b/tests/data/sample_event.json @@ -0,0 +1,123 @@ +{ + "event_type": "leak", + "event_source": "DotEnvConfigPlugin", + "event_pipeline": [ + "ip4scout", + "l9tcpid", + "l9explore", + "DotEnvConfigPlugin" + ], + "event_fingerprint": "ab2848eed8451d0ea0d48a691126d1aeab2848eed8451d0ea0d48a691126d1ae", + "ip": "127.0.0.1", + "host": "site1.example.com", + "reverse": "ptr1.example.com", + "port": "8080", + "mac": "", + "vendor": "", + "transport": [ + "tcp", + "tls", + "http" + ], + "protocol": "https", + "http": { + "root": "/site1", + "url": "/site1/.env", + "status": 200, + "length": 12423, + "header": { + "Content-Type": "application/text", + "Server": "Apache" + }, + "title": "Apache welcome page", + "favicon_hash": "e7bc546316d2d0ec13a2d3117b13468f5e939f95" + }, + "summary": "GET /... qwerqwer", + "time": "0001-01-01T00:00:00Z", + "ssl": { + "detected": true, + "enabled": true, + "jarm": "29d29d00029d29d21c41d41d00041dba71dd2df645850cf5f0b5af18a5fdcf", + "cypher_suite": "TLS_AES_128_GCM_SHA256", + "version": "TLSv1.3", + "certificate": { + "cn": "example.com", + "domain": [ + "site.example.com", + "admin.example.com" + ], + "fingerprint": "e998e371dd4678c9113e196bc5e4a5e901455750c6dbc9985c84403b91055260", + "key_algo": "RSA", + "key_size": 2048, + "issuer_name": "Rapid SSL", + "not_before": "0001-01-01T00:00:00Z", + "not_after": "0001-01-01T00:00:00Z", + "valid": false + } + }, + "ssh": { + "fingerprint": "", + "version": 0, + "banner": "", + "motd": "" + }, + "service": { + "credentials": { + "noauth": true, + "username": "", + "password": "", + "key": "", + "raw": "SSBhbSBhIGtleQo=" + }, + "software": { + "name": "Apache", + "version": "2.2.4", + "os": "Ubuntu", + "modules": [ + { + "name": "PHP", + "version": "4.4.2", + "fingerprint": "php-4-4-2" + } + ], + "fingerprint": "apache-2-2-4" + } + }, + "leak": { + "stage": "open", + "type": "configuration", + "severity": "medium", + "dataset": { + "rows": 4, + "files": 1, + "size": 13223, + "collections": 1, + "infected": false, + "ransom_notes": [ + "Do this", + "Don't do that", + "We love GDPR" + ] + } + }, + "tags": [ + "plc" + ], + "geoip": { + "continent_name": "", + "region_iso_code": "", + "city_name": "", + "country_iso_code": "", + "country_name": "", + "region_name": "", + "location": { + "lat": 0, + "lon": 0 + } + }, + "network": { + "organization_name": "", + "asn": 0, + "network": "" + } +} diff --git a/tests/test.rs b/tests/test.rs new file mode 100644 index 0000000..49c5c4a --- /dev/null +++ b/tests/test.rs @@ -0,0 +1,20 @@ +use l9event::L9Event; +use std::fs; + +#[test] +fn test_deserialize_l9event_from_file() { + // Path to the JSON file + let file_path = "tests/data/sample_event.json"; + + // Read the JSON file + let json_content = fs::read_to_string(file_path).expect("Failed to read the JSON file"); + // Deserialize into L9Event + let event: L9Event = + serde_json::from_str(&json_content).expect("Failed to deserialize the JSON content"); + + // Verify some fields (adjust based on the actual file contents) + assert_eq!(event.event_type, "leak"); + assert_eq!(event.host, "site1.example.com"); + assert_eq!(event.ip, "127.0.0.1"); + assert_eq!(event.http.status, 200); +}