From 20ce3abe2a2a486153e4b1a22e218711f1d96836 Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Fri, 17 Jul 2026 07:14:16 -0700 Subject: [PATCH] ioc search: expose --info locations, --limit, --case-sensitive, --wildcards The single-IOC 'ioc search' command always called the API with the default info=summary, so users could only ever see prevalence counts with no way to get the sensor IDs that observed the IOC. The SDK (search_ioc), API gateway and backend all already support info=locations; only the CLI flags were missing. batch-search already exposed --info/--limit. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01F2QkWSTn8PEgyKvs7epe7f --- limacharlie/commands/ioc.py | 21 ++++++++++++++++----- tests/integration/test_cli_comprehensive.py | 7 +++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/limacharlie/commands/ioc.py b/limacharlie/commands/ioc.py index 851e345b..dcb3cb27 100644 --- a/limacharlie/commands/ioc.py +++ b/limacharlie/commands/ioc.py @@ -94,13 +94,20 @@ def group() -> None: service_name - Service/daemon name package_name - Installed package name -The result includes prevalence data showing which sensors observed -the IOC, how many times, and when (first/last seen). +--info controls the response shape: + summary prevalence counts per time bucket (default). + locations the list of sensor IDs that observed the IOC, with + first/last seen times and hostnames. Use --limit to cap + the number of entries returned. + +--wildcards treats the value as a pattern where '%' matches anything +(e.g. '10.10.%'). --case-insensitive disables case-sensitive matching. Examples: limacharlie ioc search --type domain --value evil.com - limacharlie ioc search --type ip --value 1.2.3.4 + limacharlie ioc search --type ip --value 1.2.3.4 --info locations limacharlie ioc search --type file_hash --value abc123... + limacharlie ioc search --type ip --value '10.10.%' --info locations --wildcards """ register_explain("ioc.search", _EXPLAIN_SEARCH) @@ -108,11 +115,15 @@ def group() -> None: @group.command() @click.option("--type", "ioc_type", required=True, help="IOC type (domain, ip, file_hash, file_path, etc.).") @click.option("--value", required=True, help="IOC value to search for.") +@click.option("--info", "info", type=click.Choice(["summary", "locations"]), default="summary", show_default=True, help="Response shape: 'summary' counts only, or 'locations' to return the sensor IDs that observed the IOC.") +@click.option("--limit", type=int, default=None, help="Max number of results returned.") +@click.option("--case-sensitive/--case-insensitive", default=True, show_default=True, help="Case-sensitive matching of the IOC value.") +@click.option("--wildcards", is_flag=True, default=False, help="Treat the value as a pattern where '%' matches anything.") @pass_context -def search(ctx: click.Context, ioc_type: str, value: str) -> None: +def search(ctx: click.Context, ioc_type: str, value: str, info: str, limit: int | None, case_sensitive: bool, wildcards: bool) -> None: org = _get_org(ctx) insight = Insight(org) - data = insight.search_ioc(ioc_type, value) + data = insight.search_ioc(ioc_type, value, info=info, case_sensitive=case_sensitive, wildcards=wildcards, limit=limit) _output(ctx, data) diff --git a/tests/integration/test_cli_comprehensive.py b/tests/integration/test_cli_comprehensive.py index 3c9ee9fa..a8c94f4e 100644 --- a/tests/integration/test_cli_comprehensive.py +++ b/tests/integration/test_cli_comprehensive.py @@ -631,6 +631,13 @@ def test_search(self, oid, key): ]) assert result.exit_code == 0 + def test_search_locations(self, oid, key): + result, _ = _invoke(oid, key, [ + "ioc", "search", "--type", "domain", "--value", "example.com", + "--info", "locations", "--limit", "10", + ]) + assert result.exit_code == 0 + def test_hosts(self, oid, key): result, _ = _invoke(oid, key, [ "ioc", "hosts", "--hostname", "nonexistent-test-host-12345",