Skip to content

feat(query): group findings by field with aggregation - #1304

Open
ocervell wants to merge 4 commits into
mainfrom
feat/query-group-aggregate
Open

feat(query): group findings by field with aggregation#1304
ocervell wants to merge 4 commits into
mainfrom
feat/query-group-aggregate

Conversation

@ocervell

@ocervell ocervell commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Closes #1219.

Adds a --group option to secator q and secator report show that groups findings by one or more fields with auto-aggregation, done on the processing side after the query (no backend query changes), as the issue suggested.

Usage

  • secator q vulnerability --group — group vulnerabilities by their default field(s).
  • secator q vulnerability --group severity — group by an explicit field.
  • Bare --group uses per-type defaults; --group "<field>[,<field>]" overrides.

What it does

  • New _group_by / _group_aggregate class attrs on OutputType, set for the three types the issue lists:
    • Vulnerability → group by name, aggregate matched_at
    • Tag → group by category,name, aggregate match
    • Technology → group by product, aggregate match
  • group_findings() collapses each group to the newest finding, keeping a _group_count and a truncated list of the aggregated field's values (.. and X more past 5).
  • Console output shows (count: N) next to each grouped line.
  • An Info message per grouped type is printed at the end (<type> grouped by <field>. To show complete results, remove the --group option.).

Degradation

  • Result types without _group_by (and non-finding results) pass through unchanged.
  • --group <field> with no groupable finding types present → warning, no silent mis-answer.
  • --group combined with --format is ignored with a warning (format already reshapes output).

Notes / assumptions

  • Grouping happens post-query for all backends (local/mongodb/api/sqlite) since it operates on the returned findings — no $group pipeline needed, matching the issue's "processing side before displaying" guidance.
  • The issue's "aggregate: matched_at" for Technology is a typo — Technology has no matched_at field; its location field is match, so that is used (same as Tag).
  • Scope: implemented the shared --group path for secator q / report show. The issue's hedged "secator r list should probably default to grouped vuln counts" is a separate command/code path and was left out to keep scope tight — easy follow-up if wanted.

Test

Added TestGroupFindings in tests/unit/test_query_utils.py (group-by-name + aggregate + truncation asserts). secator test unit query/report/output-type suites pass (240 tests).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an optional --group flag to query and report display commands.
    • Group results by selected fields or recommended defaults for supported finding types.
    • Display grouped result counts and summarized aggregated values.
    • Show guidance when grouping is unavailable or used with formatted output.
  • Tests

    • Added coverage for grouping, aggregation, and truncated value displays.

Add a --group option to `secator q` and `secator report show` that collapses
findings by one or more fields (processing-side, post-query) with
auto-aggregation of a per-type field.

- New _group_by / _group_aggregate class attrs on OutputType, set for
  Vulnerability (name / matched_at), Tag (category,name / match) and
  Technology (product / match).
- --group is an optional-value option: bare --group uses the per-type
  defaults; --group "<field>[,<field>]" overrides the group fields.
- group_findings() helper collapses each group to the newest finding, keeps a
  _group_count and a truncated list of aggregated values (".. and X more").
- Console exporter shows "(count: N)" next to grouped lines; an Info message
  per grouped type is printed at the end.
- Types without _group_by (or non-finding results) pass through unchanged;
  --group with an explicit field but no groupable types warns; --group is
  ignored (with a warning) when --format is used.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWZJdNgJKrn1XHkENKqVo7
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f22f6f1f-3c27-442c-a603-80ebbf4597f7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Changes

Finding grouping

Layer / File(s) Summary
Grouping contracts and defaults
secator/output_types/_base.py, secator/output_types/tag.py, secator/output_types/technology.py, secator/output_types/vulnerability.py
Output types define default grouping and aggregation fields.
Finding grouping and validation
secator/query/utils.py, tests/unit/test_query_utils.py
Findings are grouped by fields, newest representatives are selected, aggregate values are truncated, and counts are tested.
CLI integration and grouped output
secator/cli.py, secator/exporters/console.py
query and report show accept --group, apply grouping when appropriate, emit messages, and render grouped counts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Poem

A rabbit taps fields in a neat little row,
Groups findings together wherever they go.
Counts softly appear in the console’s light,
With matches trimmed tidy and values just right.
“Hop!” says the bunny, “the report’s now in sight.”

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers secator q and report show, but it misses the linked secator r list default grouped vuln counts requirement. Add the grouped default output for secator r list (vulnerability counts by name), or clarify that it is intentionally out of scope.
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: grouping findings by field with aggregation.
Out of Scope Changes check ✅ Passed The changes stay within the grouping/aggregation feature and its supporting output types, CLI, and tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/query-group-aggregate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread tests/unit/test_query_utils.py Dismissed
Comment thread tests/unit/test_query_utils.py Dismissed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
secator/query/utils.py (1)

558-612: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log load failures instead of silently swallowing them.

The except Exception: pass at lines 600-603 silently degrades grouping: if cls.load(rep) fails, the representative stays a dict, so the aggregate field is never set (line 604 guards on not isinstance(rep, dict)) and _group_count is never set (line 609). The finding appears ungrouped with no count or aggregated values, and no diagnostic is emitted.

Ruff flags this as S110 (try-except-pass) and BLE001 (blind exception). Consider logging the failure at debug level so schema mismatches or data issues are traceable.

♻️ Proposed fix
             try:
                 rep = cls.load(rep)
-            except Exception:
-                pass
+            except Exception as exc:
+                logger.debug('group_findings: failed to load %s representative: %s', rep.get('_type'), exc)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@secator/query/utils.py` around lines 558 - 612, In group_findings, replace
the silent broad exception around cls.load(rep) with a debug-level log that
includes the exception and relevant finding/type context; use the module’s
existing logger or add an appropriate logger without introducing a blind
except-pass. Preserve the fallback behavior while making load failures traceable
and satisfying Ruff S110/BLE001.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@secator/cli.py`:
- Around line 1459-1481: Update the warning condition in the group-processing
block after grouped_types is populated so it triggers when grouping was
requested, including bare --group where group == '', and no groupable types were
found; preserve the existing warning message and avoid warning when --group was
not provided.

---

Nitpick comments:
In `@secator/query/utils.py`:
- Around line 558-612: In group_findings, replace the silent broad exception
around cls.load(rep) with a debug-level log that includes the exception and
relevant finding/type context; use the module’s existing logger or add an
appropriate logger without introducing a blind except-pass. Preserve the
fallback behavior while making load failures traceable and satisfying Ruff
S110/BLE001.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0fbd523e-a333-42b1-9454-629ffde1cbe1

📥 Commits

Reviewing files that changed from the base of the PR and between 8fcafa6 and 96ee3ab.

📒 Files selected for processing (8)
  • secator/cli.py
  • secator/exporters/console.py
  • secator/output_types/_base.py
  • secator/output_types/tag.py
  • secator/output_types/technology.py
  • secator/output_types/vulnerability.py
  • secator/query/utils.py
  • tests/unit/test_query_utils.py

Comment thread secator/cli.py
Comment on lines +1459 to +1481

# Group findings by field(s) with auto-aggregation (processing-side, post-query).
# `group is None` => disabled; `group == ''` => per-type defaults; else explicit field(s).
grouped_types = []
if group is not None and not fmt:
from secator.query.utils import group_findings
user_group_by = [f.strip() for f in group.split(',') if f.strip()]
type_map = {cls.get_name(): cls for cls in FINDING_TYPES}
for type_name, items in report.data['results'].items():
cls = type_map.get(type_name)
if not items or cls is None:
continue
group_by = user_group_by or list(getattr(cls, '_group_by', ()) or ())
if not group_by:
continue
aggregate_field = getattr(cls, '_group_aggregate', None)
report.data['results'][type_name] = group_findings(items, group_by, aggregate_field)
grouped_types.append((type_name, ', '.join(group_by)))
if group and not grouped_types:
console.print(Warning(message=f'--group "{group}": no groupable finding types in results'))
elif group is not None and fmt:
console.print(Warning(message='--group is ignored when --format is used'))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Bare --group with no groupable types produces no warning.

Line 1477 checks if group and not grouped_types, but when bare --group is used, group == '' (falsy), so the warning never fires. Users who run --group with only non-groupable result types (e.g., URLs, domains) get no feedback that grouping was a no-op.

The PR objectives state grouping should "warn when grouping is unavailable." Fix the condition to also cover the bare-flag case.

🐛 Proposed fix
-		if group and not grouped_types:
-			console.print(Warning(message=f'--group "{group}": no groupable finding types in results'))
+		if not grouped_types:
+			group_desc = f' "{group}"' if group else ''
+			console.print(Warning(message=f'--group{group_desc}: no groupable finding types in results'))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Group findings by field(s) with auto-aggregation (processing-side, post-query).
# `group is None` => disabled; `group == ''` => per-type defaults; else explicit field(s).
grouped_types = []
if group is not None and not fmt:
from secator.query.utils import group_findings
user_group_by = [f.strip() for f in group.split(',') if f.strip()]
type_map = {cls.get_name(): cls for cls in FINDING_TYPES}
for type_name, items in report.data['results'].items():
cls = type_map.get(type_name)
if not items or cls is None:
continue
group_by = user_group_by or list(getattr(cls, '_group_by', ()) or ())
if not group_by:
continue
aggregate_field = getattr(cls, '_group_aggregate', None)
report.data['results'][type_name] = group_findings(items, group_by, aggregate_field)
grouped_types.append((type_name, ', '.join(group_by)))
if group and not grouped_types:
console.print(Warning(message=f'--group "{group}": no groupable finding types in results'))
elif group is not None and fmt:
console.print(Warning(message='--group is ignored when --format is used'))
# Group findings by field(s) with auto-aggregation (processing-side, post-query).
# `group is None` => disabled; `group == ''` => per-type defaults; else explicit field(s).
grouped_types = []
if group is not None and not fmt:
from secator.query.utils import group_findings
user_group_by = [f.strip() for f in group.split(',') if f.strip()]
type_map = {cls.get_name(): cls for cls in FINDING_TYPES}
for type_name, items in report.data['results'].items():
cls = type_map.get(type_name)
if not items or cls is None:
continue
group_by = user_group_by or list(getattr(cls, '_group_by', ()) or ())
if not group_by:
continue
aggregate_field = getattr(cls, '_group_aggregate', None)
report.data['results'][type_name] = group_findings(items, group_by, aggregate_field)
grouped_types.append((type_name, ', '.join(group_by)))
if not grouped_types:
group_desc = f' "{group}"' if group else ''
console.print(Warning(message=f'--group{group_desc}: no groupable finding types in results'))
elif group is not None and fmt:
console.print(Warning(message='--group is ignored when --format is used'))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@secator/cli.py` around lines 1459 - 1481, Update the warning condition in the
group-processing block after grouped_types is populated so it triggers when
grouping was requested, including bare --group where group == '', and no
groupable types were found; preserve the existing warning message and avoid
warning when --group was not provided.

@ocervell

Copy link
Copy Markdown
Contributor Author

@claude address coderabbit review comment

@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown

Claude finished @ocervell's task in 3m 7s —— View job


Addressing CodeRabbit review comments

  • Gather context (read cli.py and query/utils.py relevant sections)
  • Fix bare --group warning condition in secator/cli.py
  • Log load failures instead of silently swallowing them in secator/query/utils.py
  • Run lint/unit tests — could not run, see note below
  • Commit and push

Changes made (commit b4880e4)

  1. secator/cli.py — the "no groupable finding types in results" warning only fired when group was truthy, so bare --group (which is '', falsy) with only non-groupable result types silently produced no feedback. Changed the condition to fire whenever grouped_types ends up empty, regardless of whether --group was bare or given an explicit field list, while keeping the message wording (quoting the field list only when one was given).

  2. secator/query/utils.py (group_findings) — replaced the bare except Exception: pass around cls.load(rep) with a debug(...) call (using this repo's existing secator.utils.debug helper, sub='query', consistent with the other debug() calls already in this file) so schema/load failures during grouping are traceable instead of silently degrading the representative to an unloaded dict.

Both fixes correspond to CodeRabbit's actionable comment (bare --group warning) and nitpick (silent exception swallowing, Ruff S110/BLE001) on this PR. The CodeQL "Incomplete URL substring sanitization" comments on tests/unit/test_query_utils.py are from a different tool (GitHub Advanced Security, not CodeRabbit) and weren't part of this request, so left untouched.

Note on verification

This sandboxed environment blocks Python execution (python3 -c, python3 -m, secator ..., pip etc. all require approval that isn't available here), so I was unable to run secator test lint or secator test unit to verify. I manually reviewed the diff for correctness (line length under flake8's 120-char limit, debug() call signature matches existing usages in the same file). Please run secator test lint and secator test unit --test test_query_utils to confirm before merging.
— branch feat/query-group-aggregate

github-actions Bot and others added 2 commits July 13, 2026 10:56
- secator/cli.py: fire the "no groupable finding types" warning for
  bare --group too (group == '' was falsy, so the warning never fired
  when only per-type defaults were requested).
- secator/query/utils.py: log load failures in group_findings via
  debug() instead of silently swallowing them (Ruff S110/BLE001).

Co-authored-by: Olivier Cervello <9629314+ocervell@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(query): group findings by field + aggregation

2 participants