Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions UDM_SEARCH_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# UDM Search Integration for Logstory

## Overview

Implemented **Option 1: Extend `_get_log_content()` to support UDM Search queries**.

This feature allows logstory to fetch UDM events directly from Chronicle via UDM search queries, then replay them with updated timestamps into the ingestion pipeline—without needing pre-existing `.log` files.

## Changes Made

### 1. Core Logic (`src/logstory/main.py`)

#### New Function: `_search_udm()`
- Executes UDM search queries against Chronicle API
- Parameters:
- `http_client`: Authenticated HTTP session
- `customer_id`: Chronicle customer ID
- `query`: UDM search query string
- `start_time`, `end_time`: Time range for search (defaults to last 30 days)
- `region`: Chronicle region (US or EU)
- Returns results as JSON lines (one UDM event per line)
- Handles API errors and logging

#### Modified Function: `_get_log_content()`
- Now accepts optional UDM search parameters:
- `udm_query`: UDM search query to execute
- `http_client`: Authenticated client for the query
- `customer_id`: For API calls
- `region`: Chronicle region
- Logic flow:
- If `udm_query` provided → execute UDM search and return results
- Otherwise → fall back to existing file-based logic (GCS or local filesystem)
- Maintains full backward compatibility

#### Modified Function: `usecase_replay_logtype()`
- Added parameters to support UDM queries
- Passes UDM parameters down to `_get_log_content()`
- Works seamlessly with existing timestamp replacement and ingestion pipeline

### 2. CLI Commands (`src/logstory/logstory.py`)

#### New Command: `replay from-udm-search`
```bash
logstory replay from-udm-search "<UDM_QUERY>" \
--customer-id=<ID> \
--credentials-path=<PATH> \
--timestamp-delta=1d
```

Example:
```bash
logstory replay from-udm-search "metadata.event_type='PROCESS_EXECUTION'" \
--customer-id=01234567-0123-4321-abcd-01234567890a \
--credentials-path=/path/to/credentials.json
```

#### New Internal Function: `_replay_from_udm()`
- Orchestrates UDM query execution and replay
- Applies timestamp shifting via existing pipeline
- Injects ingestion labels identifying the source as UDM search
- Outputs verification query for finding replayed events in Chronicle

### 3. Configuration (`src/logstory/logtypes_events_timestamps.yaml`)

Added timestamp configuration for UDM events:
```yaml
UDM_EVENTS:
api: udmevents
timestamps:
- name: created_time
base_time: true
pattern: '("created_time":\s*"?)(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(.\d+Z?\s*"?)'
dateformat: "%Y-%m-%dT%H:%M:%S"
group: 2
- name: generic_event_timestamp
pattern: '("(?:event_time|timestamp|created_at|time)":\s*"?)(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(.\d+Z?\s*"?)'
dateformat: "%Y-%m-%dT%H:%M:%S"
group: 2
```

- **api**: `udmevents` (uses native UDM ingestion)
- **timestamps**: Pattern matching for common timestamp field names in UDM events
- Supports ISO 8601 format timestamps (RFC 3339)

### 4. Tests (`tests/test_udm_search.py`)

Created comprehensive test suite:
- `test_search_udm_formats_results_as_jsonl()` - Verifies UDM API results conversion
- `test_get_log_content_with_udm_query()` - Tests UDM query path in content retrieval
- `test_get_log_content_without_udm_query()` - Ensures backward compatibility
- `test_udm_query_requires_http_client()` - Validates error handling

## Data Flow

```
UDM Search Query
_search_udm() → Chronicle API
JSON Lines (one UDM event per line)
usecase_replay_logtype()
Timestamp extraction & replacement (via existing regex patterns)
post_entries() → Ingestion API (udmevents)
Chronicle with updated timestamps
Ingestion Labels:
- log_replay: true
- replayed_from: logstory
- source_type: udm_search
```

## Key Design Decisions

1. **No new file dependencies**: UDM results flow directly through logstory's existing replay pipeline
2. **Backward compatible**: Existing file-based replay unchanged; UDM is additive
3. **Timestamp handling**: Reuses pattern matching from YAML configs; ISO 8601 patterns added
4. **API abstraction**: Uses authenticated `http_client` from existing auth system
5. **User-facing**: CLI command is simple and discoverable

## Usage Examples

### Basic UDM search replay
```bash
logstory replay from-udm-search "metadata.event_type='PROCESS_EXECUTION'"
```

### With custom timestamp shift
```bash
logstory replay from-udm-search "event.principal.ip_address='192.168.1.1'" \
--timestamp-delta=7d
```

### With environment variables
```bash
export LOGSTORY_CUSTOMER_ID=<ID>
export LOGSTORY_CREDENTIALS_PATH=/path/to/credentials.json
logstory replay from-udm-search "metadata.ingestion_time >= '2024-01-01'"
```

## Verification

To find replayed logs:
```
metadata.ingested_timestamp.seconds >= <LOGSTORY_EXECUTION_TIME>
metadata.ingestion_labels["source_type"]="udm_search"
```

## Future Enhancements

- Support for UDM Search time range parameters in CLI
- Batch processing for large result sets
- Result pagination support
- Custom timestamp field mapping per query
- CSV export option (phase 2 of original issue)
106 changes: 106 additions & 0 deletions UDM_SEARCH_QUICKSTART.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# UDM Search Quick Start

## What's New?

Logstory now supports replaying logs directly from UDM searches without requiring pre-existing `.log` files. This is **Option 1** from issue #27: extend logstory's ingestion pipeline to accept UDM search results and apply timestamp shifting.

## Quick Examples

### Search for process execution events and replay with 1-day shift
```bash
logstory replay from-udm-search "metadata.event_type='PROCESS_EXECUTION'" \
--customer-id=YOUR_CUSTOMER_ID \
--credentials-path=/path/to/credentials.json
```

### Search for network connections from a specific IP
```bash
logstory replay from-udm-search "event.principal.ip_address='192.168.1.100'" \
--timestamp-delta=7d
```

### Search with time filtering
```bash
logstory replay from-udm-search "metadata.event_type='USER_LOGIN' AND metadata.severity='WARNING'"
```

### Use environment variables for credentials
```bash
export LOGSTORY_CUSTOMER_ID=01234567-0123-4321-abcd-01234567890a
export LOGSTORY_CREDENTIALS_PATH=/path/to/credentials.json
export LOGSTORY_REGION=US

logstory replay from-udm-search "metadata.ingestion_time >= '2024-01-01'"
```

## How It Works

1. **Query UDM** — Logstory queries Chronicle's UDM Search API with your query
2. **Get Results** — Chronicle returns matching UDM events as JSON
3. **Shift Timestamps** — Logstory applies your timestamp delta (e.g., `--timestamp-delta=1d`)
4. **Ingest** — Updated events are replayed back into Chronicle
5. **Verify** — Special ingestion labels mark events as replayed from UDM search

## Finding Your Replayed Logs

After replay, search Chronicle with:
```
metadata.ingested_timestamp.seconds >= <EXECUTION_TIME>
metadata.ingestion_labels["source_type"]="udm_search"
```

## Command Options

```
logstory replay from-udm-search [OPTIONS] QUERY

Options:
--customer-id TEXT Chronicle customer ID
--credentials-path TEXT Path to service account JSON
--region TEXT Chronicle region (US or EU)
--timestamp-delta TEXT Time shift (e.g., 1d, 2h, 30m)
--env-file TEXT Path to .env file
--api-type TEXT API type (rest or legacy)
--project-id TEXT GCP project ID for REST API
--forwarder-name TEXT Forwarder name for REST API
--impersonate-service-account Service account to impersonate
--local-file-output Write to local files instead of API
```

## Use Cases

- **Incident Response** — Replay security events with current timestamps to test detections
- **Testing** — Validate Chronicle configuration changes with historical UDM data
- **Demos** — Show security scenarios with realistic event sequences
- **Research** — Analyze subsets of historical data in a controlled replay scenario

## What Changed in the Code?

See `UDM_SEARCH_IMPLEMENTATION.md` for full technical details.

### Files Modified:
- `src/logstory/main.py` — Core UDM search and replay logic
- `src/logstory/logstory.py` — New CLI command
- `src/logstory/logtypes_events_timestamps.yaml` — UDM event timestamp patterns
- `tests/test_udm_search.py` — Unit tests

### Key New Functions:
- `_search_udm()` — Execute UDM search queries
- `_replay_from_udm()` — Orchestrate UDM search + replay

## FAQ

**Q: Do I still need `.log` files?**
A: No, not for UDM search replay. Regular file-based replay still works as before.

**Q: Can I combine multiple UDM searches?**
A: Use a complex query: `(metadata.event_type='PROCESS_EXECUTION' OR metadata.event_type='FILE_MODIFICATION')`

**Q: What timestamp delta values are supported?**
A: Combinations like `1d`, `2h`, `30m`, or `1d2h30m`

**Q: How far back can I search?**
A: By default, the last 30 days. Customize with start/end times (future enhancement).

**Q: Are replayed events marked differently?**
A: Yes, they have ingestion labels: `replayed_from=logstory` and `source_type=udm_search`
101 changes: 101 additions & 0 deletions src/logstory/logstory.py
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,52 @@
_replay_usecases(usecases, logtype_list, entities, timestamp_delta, local_file_output)


@replay_app.command("from-udm-search")
def replay_from_udm_search(

Check failure on line 1044 in src/logstory/logstory.py

View workflow job for this annotation

GitHub Actions / Lint and Format

ruff (D417)

src/logstory/logstory.py:1044:5: D417 Missing argument descriptions in the docstring for `replay_from_udm_search`: `api_type`, `credentials_path`, `customer_id`, `env_file`, `forwarder_name`, `impersonate_service_account`, `local_file_output`, `project_id`, `region`, `timestamp_delta`
query: str = typer.Argument(..., help="UDM search query"),
env_file: str | None = EnvFileOption,
credentials_path: str | None = CredentialsOption,
customer_id: str | None = CustomerIdOption,
region: str | None = RegionOption,
timestamp_delta: str | None = TimestampDeltaOption,
local_file_output: bool = LocalFileOutputOption,
api_type: str | None = ApiTypeOption,
project_id: str | None = ProjectIdOption,
forwarder_name: str | None = ForwarderNameOption,
impersonate_service_account: str | None = ImpersonateServiceAccountOption,
):
"""Replay logs from a UDM search query.

Args:
query: UDM search query (e.g., "metadata.event_type='PROCESS_EXECUTION'")
"""
final_credentials, final_customer_id, final_region = _load_and_validate_params(
env_file,
credentials_path,
customer_id,
region,
impersonate_service_account,
api_type,
)
_set_environment_vars(
final_credentials,
final_customer_id,
final_region,
api_type,
project_id,
forwarder_name,
impersonate_service_account,
)

_replay_from_udm(
query,
timestamp_delta,
final_customer_id,
final_region,
local_file_output,
)


def _replay_usecases(
usecases: list[str],
logtypes: list[str] | str,
Expand Down Expand Up @@ -1083,6 +1129,61 @@
""")


def _replay_from_udm(
query: str,
timestamp_delta: str | None,
customer_id: str,
region: str,
local_file_output: bool = False,
):
"""Replay logs from UDM search query.

Args:
query: UDM search query.
timestamp_delta: Timestamp delta for time shifts.
customer_id: Chronicle customer ID.
region: Chronicle region.
local_file_output: Whether to write to local files instead of API.
"""
logstory_exe_time = _get_current_time()

typer.echo(f"Executing UDM search query: {query}")

# Get authenticated HTTP client
if not imported_main.http_client:
raise RuntimeError("No HTTP client available for UDM search")

# We need a synthetic log_type and use_case for the replay pipeline
# Since this is a direct UDM search, we'll use generic names
use_case = "UDM_SEARCH"
log_type = "UDM_EVENTS"

try:
old_base_time = imported_main.usecase_replay_logtype(

Check failure on line 1162 in src/logstory/logstory.py

View workflow job for this annotation

GitHub Actions / Lint and Format

ruff (F841)

src/logstory/logstory.py:1162:5: F841 Local variable `old_base_time` is assigned to but never used help: Remove assignment to unused variable `old_base_time`
use_case,
log_type,
logstory_exe_time,
timestamp_delta=timestamp_delta,
entities=False,
local_file_output=local_file_output,
udm_query=query,
http_client=imported_main.http_client,
customer_id=customer_id,
region=region,
)

typer.echo(f"Successfully replayed UDM search results with timestamp delta: {timestamp_delta}")
typer.echo(f"""UDM Search for the replayed logs:
metadata.ingested_timestamp.seconds >= {int(logstory_exe_time.timestamp())}
metadata.ingestion_labels["log_replay"]="true"
metadata.ingestion_labels["replayed_from"]="logstory"
metadata.ingestion_labels["source_type"]="udm_search"
""")
except Exception as e:
typer.echo(f"Error replaying UDM search: {e}", err=True)
raise typer.Exit(code=1)

Check failure on line 1184 in src/logstory/logstory.py

View workflow job for this annotation

GitHub Actions / Lint and Format

ruff (B904)

src/logstory/logstory.py:1184:5: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling


def entry_point():
"""Main entry point for the CLI."""
app()
Expand Down
13 changes: 13 additions & 0 deletions src/logstory/logtypes_events_timestamps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -748,3 +748,16 @@ GITHUB:
pattern: '("@timestamp":)(\d{10})'
dateformat: 'epoch'
group: 2

UDM_EVENTS:
api: udmevents
timestamps:
- name: created_time
base_time: true
pattern: '("created_time":\s*"?)(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(.\d+Z?\s*"?)'
dateformat: "%Y-%m-%dT%H:%M:%S"
group: 2
- name: generic_event_timestamp
pattern: '("(?:event_time|timestamp|created_at|time)":\s*"?)(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(.\d+Z?\s*"?)'
dateformat: "%Y-%m-%dT%H:%M:%S"
group: 2
Loading
Loading