mirror of
https://github.com/domainaware/parsedmarc.git
synced 2026-08-02 13:42:19 +00:00
Add Google SecOps (Chronicle) output via the v1 events.import API
Sends parsed reports to a Google SecOps instance as pre-normalized UDM
events through the GA v1 Chronicle API:
POST https://chronicle.{region}.rep.googleapis.com/v1/{parent}/events:import
Pre-normalized events bypass SecOps's server-side (CBN) parsing layer,
so no parser needs to be installed in the tenant; the CBN parser in
google_secops_parser/ remains the collector-based alternative for
deployments that want raw-log retention, and both paths emit the same
UDM shape and additional-field keys so searches port between them.
Google's ingestion docs recommend the UDM-events path when possible.
Implementation notes, all grounded in the v1 reference docs:
- Auth is standard Google Cloud IAM (Chronicle API Editor role /
chronicle.events.import permission): a service account key file when
[gsecops] credentials_file is set, Application Default Credentials
otherwise. google-auth was already a transitive requirement via
mailsuite[gmail]; it is now declared directly.
- Batches follow the documented best practices (1,000 events per
request, 60 s timeout). events.import is all-or-nothing — one invalid
event rejects the whole request — so a rejected batch is bisected to
isolate invalid events; valid events are still delivered and the drop
count is raised as an output error at the end.
- to and subject are repeated fields in the UDM Email message;
SecurityResult action and category are repeated enums.
- additional is a protobuf Struct, so counts and ASNs stay numbers and
alignment flags stay booleans — range-queryable without the CBN
string-hop.
New [gsecops] config section (project_id, instance_id, region,
credentials_file) is wired through the INI schema, _parse_config,
Namespace defaults, PARSEDMARC_GSECOPS_* env vars, the usage docs, and
per-batch client construction (SIGHUP-reload safe by construction).
Tests build events from real sample reports and assert on the payloads
sent through a mocked AuthorizedSession, including batching and the
400-bisect path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
95e881bdb3
commit
fba083f351
@@ -3752,6 +3752,64 @@ class TestParseConfigWebhook(unittest.TestCase):
|
||||
self.assertEqual(opts.webhook_failure_url, "https://old.example.com/fail")
|
||||
|
||||
|
||||
class TestParseConfigGsecops(unittest.TestCase):
|
||||
def test_gsecops_complete(self):
|
||||
from parsedmarc.cli import _parse_config
|
||||
|
||||
cp = _config_with(
|
||||
"gsecops",
|
||||
{
|
||||
"project_id": "my-project",
|
||||
"instance_id": "my-instance",
|
||||
"region": "europe",
|
||||
"credentials_file": "~/gsecops-key.json",
|
||||
},
|
||||
)
|
||||
opts = _opts()
|
||||
_parse_config(cp, opts)
|
||||
self.assertEqual(opts.gsecops_project_id, "my-project")
|
||||
self.assertEqual(opts.gsecops_instance_id, "my-instance")
|
||||
self.assertEqual(opts.gsecops_region, "europe")
|
||||
# file path config values must be wrapped with _expand_path
|
||||
self.assertEqual(
|
||||
opts.gsecops_credentials_file,
|
||||
os.path.expanduser("~/gsecops-key.json"),
|
||||
)
|
||||
|
||||
def test_gsecops_missing_project_id_raises(self):
|
||||
from parsedmarc.cli import ConfigurationError, _parse_config
|
||||
|
||||
cp = _config_with("gsecops", {"instance_id": "my-instance"})
|
||||
with self.assertRaises(ConfigurationError):
|
||||
_parse_config(cp, _opts())
|
||||
|
||||
def test_gsecops_missing_instance_id_raises(self):
|
||||
from parsedmarc.cli import ConfigurationError, _parse_config
|
||||
|
||||
cp = _config_with("gsecops", {"project_id": "my-project"})
|
||||
with self.assertRaises(ConfigurationError):
|
||||
_parse_config(cp, _opts())
|
||||
|
||||
def test_gsecops_env_vars_resolve_to_section(self):
|
||||
"""PARSEDMARC_GSECOPS_* env vars round-trip into the gsecops
|
||||
section via longest-prefix section matching."""
|
||||
from argparse import Namespace
|
||||
from parsedmarc.cli import _load_config, _parse_config
|
||||
|
||||
env = {
|
||||
"PARSEDMARC_GSECOPS_PROJECT_ID": "env-project",
|
||||
"PARSEDMARC_GSECOPS_INSTANCE_ID": "env-instance",
|
||||
"PARSEDMARC_GSECOPS_REGION": "asia-southeast1",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
config = _load_config(None)
|
||||
opts = Namespace()
|
||||
_parse_config(config, opts)
|
||||
self.assertEqual(opts.gsecops_project_id, "env-project")
|
||||
self.assertEqual(opts.gsecops_instance_id, "env-instance")
|
||||
self.assertEqual(opts.gsecops_region, "asia-southeast1")
|
||||
|
||||
|
||||
class TestConfigureLogging(unittest.TestCase):
|
||||
"""_configure_logging is called in every child process for parallel
|
||||
parsing — if it stops attaching a handler, log output goes dark in
|
||||
|
||||
Reference in New Issue
Block a user