Skip to content

Commit 9a0a38e

Browse files
Add "Plugin" column to sigma list targets and sigma list pipelines (#87)
* Initial plan * Add Plugin column to sigma list targets and sigma list pipelines Agent-Logs-Url: https://github.com/SigmaHQ/sigma-cli/sessions/ba0da45e-4bc2-48de-88f1-4cb5e11a1846 Co-authored-by: thomaspatzke <1845601+thomaspatzke@users.noreply.github.com> * Show Sigma plugin identifier in Plugin column instead of Python package name Agent-Logs-Url: https://github.com/SigmaHQ/sigma-cli/sessions/5f95be44-fd6c-48ca-97c8-91041626a62c Co-authored-by: thomaspatzke <1845601+thomaspatzke@users.noreply.github.com>
1 parent 0f66e4d commit 9a0a38e

2 files changed

Lines changed: 76 additions & 7 deletions

File tree

sigma/cli/list.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,53 @@
1313
plugins = InstalledSigmaPlugins.autodiscover()
1414

1515

16+
def _plugin_id_from_module(module_name: str, namespace: str) -> str:
17+
"""Extract the Sigma plugin identifier from a module name.
18+
19+
Sigma backends live in ``sigma.backends.<plugin_id>.*`` and pipelines in
20+
``sigma.pipelines.<plugin_id>.*``. Splitting on '.' and picking the
21+
component after the namespace prefix returns the plugin identifier directly
22+
(e.g. ``sigma.backends.splunk.backend`` → ``splunk``).
23+
"""
24+
parts = module_name.split(".")
25+
# Expected layout: sigma . <namespace> . <plugin_id> [. submodule ...]
26+
try:
27+
idx = parts.index(namespace)
28+
plugin_id = parts[idx + 1]
29+
return plugin_id if plugin_id else "n/a"
30+
except (ValueError, IndexError):
31+
return "n/a"
32+
33+
34+
def _get_backend_plugin_id(backend_class) -> str:
35+
"""Return the Sigma plugin identifier for a backend class."""
36+
return _plugin_id_from_module(backend_class.__module__, "backends")
37+
38+
39+
def _get_pipeline_plugin_id(pipeline_obj) -> str:
40+
"""Return the Sigma plugin identifier for a pipeline object or function."""
41+
# Plain function: use its own module
42+
module_name = getattr(pipeline_obj, "__module__", None)
43+
if module_name and module_name != "sigma.pipelines.base":
44+
return _plugin_id_from_module(module_name, "pipelines")
45+
# Pipeline-decorator instance: retrieve the wrapped function's module
46+
func = getattr(pipeline_obj, "func", None)
47+
if func is not None:
48+
func_module = getattr(func, "__module__", None)
49+
if func_module:
50+
return _plugin_id_from_module(func_module, "pipelines")
51+
return "n/a"
52+
53+
54+
# Pre-compute plugin IDs for all discovered pipelines (keyed by identifier).
55+
# This must be done before the resolver resolves pipeline callables into plain
56+
# ProcessingPipeline objects, which no longer carry module information.
57+
_pipeline_plugin_ids: dict = {
58+
name: _get_pipeline_plugin_id(pipeline)
59+
for name, pipeline in plugins.pipelines.items()
60+
}
61+
62+
1663
@click.group(name="list", help="List available targets or processing pipelines.")
1764
def list_group():
1865
pass
@@ -32,13 +79,15 @@ def list_targets():
3279
"Identifier",
3380
"Target Query Language",
3481
"Processing Pipeline Required",
82+
"Plugin",
3583
)
3684
table.add_rows(
3785
[
3886
(
3987
name,
4088
fill(backend.name, width=60),
4189
"Yes" if backend.requires_pipeline else "No",
90+
_get_backend_plugin_id(backend),
4291
)
4392
for name, backend in plugins.backends.items()
4493
]
@@ -108,6 +157,7 @@ def list_pipelines(backend):
108157
"Priority",
109158
"Processing Pipeline",
110159
"Backends",
160+
"Plugin",
111161
)
112162
for name, pipeline in pipelines:
113163
if (
@@ -120,7 +170,13 @@ def list_pipelines(backend):
120170
else:
121171
backends = "all"
122172
table.add_row(
123-
(name, pipeline.priority, fill(pipeline.name, width=60), backends)
173+
(
174+
name,
175+
pipeline.priority,
176+
fill(pipeline.name, width=60),
177+
backends,
178+
_pipeline_plugin_ids.get(name, "n/a"),
179+
)
124180
)
125181
table.align = "l"
126182
click.echo(table.get_string())

tests/test_lists.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,23 @@ def test_transformation_list():
125125
assert all((name in result.stdout for name in transformations.keys()))
126126

127127

128-
def test_condition_list():
128+
def test_targets_has_plugin_column():
129129
cli = CliRunner()
130-
result = cli.invoke(list_conditions)
131-
conditions = list(rule_conditions.keys())
132-
conditions.extend(detection_item_conditions.keys())
133-
conditions.extend(field_name_conditions.keys())
134-
assert all((name in result.stdout for name in conditions))
130+
result = cli.invoke(list_targets)
131+
assert result.exit_code == 0
132+
assert "Plugin" in result.stdout
133+
# Each row should show a non-empty package name (not "n/a") for installed backends.
134+
plugins = InstalledSigmaPlugins.autodiscover()
135+
if plugins.backends:
136+
assert "n/a" not in result.stdout
137+
138+
139+
def test_pipelines_has_plugin_column():
140+
cli = CliRunner()
141+
result = cli.invoke(list_pipelines)
142+
assert result.exit_code == 0
143+
assert "Plugin" in result.stdout
144+
# Each row should show a non-empty package name (not "n/a") for installed pipelines.
145+
plugins = InstalledSigmaPlugins.autodiscover()
146+
if plugins.pipelines:
147+
assert "n/a" not in result.stdout

0 commit comments

Comments
 (0)