Skip to content

ingest_log silently drops events for AIDE (and likely other file-integrity log types) #213

@Mogriffs01

Description

@Mogriffs01

ingest_log silently drops events for AIDE (and likely other file-integrity log types) — sourceFilename field missing from LogsInlineSource payload

Summary

ChronicleClient.ingest_log(...) constructs a LogsInlineSource payload for POST .../logTypes/{log_type}/logs:import without the sourceFilename field. For at least the AIDE log type, Chronicle's ingestion pipeline silently drops submissions that omit it — the REST call returns HTTP 200 with an empty {} body (the standard queued-acknowledgement response shape), but the event never reaches the parser and never surfaces in search_udm. No error is ever surfaced to the caller.

The REST reference at projects.locations.instances.logTypes.logs/import#logsinlinesource shows sourceFilename as a field on LogsInlineSource (sibling of logs[] and forwarder). The SDK does not expose it — source_filename / sourceFilename appears zero times in src/secops/chronicle/log_ingest.py, and ChronicleClient.ingest_log's signature has no corresponding parameter.

Environment

  • secops 0.42.0 (also verified against 0.43.0 — no change to log_ingest.py between versions)
  • Tenant: Chronicle instance in region europe
  • Python 3.12
  • Authenticated as a service account with roles/chronicle.admin

Reproduction

from secops import SecOpsClient

client = SecOpsClient().chronicle(
    customer_id="...", project_id="...", region="europe",
)

# AIDE sample — valid syslog RFC5424 line, well-formed, UTF-8 clean.
# Parses to exactly 1 UDM event via run_parser against the committed parser.
log = '<133>1 2026-04-19T06:04:02.070299+00:00 host aide 1234 - - file=/etc/shadow;Mtime_new=...'

resp = client.ingest_log(
    log_type="AIDE",
    log_message=log,
    labels={"source": "smoketest", "smoketesteventid": "abcd1234"},
)
# resp == {}          — reported success
# Subsequent search_udm for smoketesteventid="abcd1234" → zero hits, indefinitely.

The submitted event never surfaces. No exception raised, no indication anything went wrong. Reproduced across > 5 probes over several hours, including with uniquely-generated synthetic content (rules out deduplication).

Production forwarder-pushed AIDE events continue to ingest and parse normally (> 10,000/day on the same tenant), so it is not a parser or tenant-level issue — it's specific to the logs:import inline path.

Root cause

The body emitted by ingest_log (per src/secops/chronicle/log_ingest.py lines 878–886 in the current main) looks like:

payload = {
    "inline_source": {
        "logs": [
            {
                "data": "<base64>",
                "log_entry_time": "...",
                "collection_time": "...",
                "labels": {"source": {"value": "smoketest"}, ...}
            }
        ],
        "forwarder": "projects/.../forwarders/<uuid>",
    }
}

There's no sourceFilename set at the inline_source level. Chronicle accepts the call (HTTP 200) but doesn't process the event.

Fix (what we had to do)

We bypassed ingest_log and built the request directly via chronicle_request, setting sourceFilename at the correct level — sibling of logs[] and forwarder:

from secops.chronicle.utils.request_utils import chronicle_request
import base64

payload = {
    "inline_source": {
        "logs": [
            {
                "data": base64.b64encode(log.encode("utf-8")).decode("ascii"),
                "log_entry_time": ts, "collection_time": ts,
                "labels": {"source": {"value": "smoketest"}, ...},
            }
        ],
        "forwarder": "projects/.../forwarders/<uuid>",
        "sourceFilename": "smoketest/AIDE/abcd1234.log",
    }
}
chronicle_request(client, "POST", "logTypes/AIDE/logs:import", json=payload)

With this change the AIDE event lands immediately and appears in search_udm within the normal sub-minute window.

Two important placement notes from trial-and-error:

  • sourceFilename belongs at inline_source level (sibling of logs + forwarder), per the REST reference.
  • Placing it inside an individual log entry (inline_source.logs[0].sourceFilename) returns HTTP 400: Unknown name "sourceFilename" at 'inline_source.logs[0]': Cannot find field.

We separately verified that adding sourceFilename to a log type that was previously ingesting fine (GOANYWHERE_MFT in our case) does not break anything — it's safe to set unconditionally. (extensively tested /s - Matt)

Suggested fix for the SDK

(AI generated, apply salt liberally - Matt)

Add a source_filename: str | None = None parameter to ChronicleClient.ingest_log() and plumb it through to the payload:

def ingest_log(
    self, log_type, log_message, ..., source_filename: str | None = None,
):
    ...
    inline_source = {
        "logs": logs,
        "forwarder": forwarder_resource,
    }
    if source_filename is not None:
        inline_source["sourceFilename"] = source_filename
    payload = {"inline_source": inline_source}

Optionally, defaulting it to a generated value (e.g. sdk-<log_type>.log) rather than omitting it would make the SDK robust against the undocumented server-side requirement — AIDE and possibly other file-integrity / file-aware log types appear to require it, and users hitting the silent-drop behaviour have no feedback mechanism pointing at the field.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions