Skip to content

feat(platform): add portable cybersecurity plugin framework - #9

Merged
sodejm merged 2 commits into
mainfrom
codex/portable-plugin-platform
Sep 21, 2026
Merged

sodejm merged 2 commits into
mainfrom
codex/portable-plugin-platform

Conversation

@sodejm

@sodejm sodejm commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Summary

  • add a canonical package schema, catalog, and cops operator CLI
  • generate portable manifests and adapters for Codex, GitHub Copilot, and Claude Code
  • add an operator-first getting-started path and a documented plugin contribution workflow
  • package the Sentinel Hunt Workbench with hunts, profiles, fixtures, evaluation, and adapter verification
  • add repository acceptance tests and Linux, macOS, and Windows contract CI

Validation

  • .venv/bin/python scripts/agent/check.py
  • 236 pytest tests passed; 5 skipped
  • 48 maintainer unit tests passed
  • all 3 package contracts and all 15 skill contracts passed
  • 576 curated Sentinel cases and 3,000 perturbations passed
  • all 144 critical mutations were detected
  • generated marketplace indexes and 29 host adapter files are current
  • git diff origin/main...HEAD --check

Validation boundaries

  • Sentinel evidence is offline/reference-evaluator evidence, not live Kusto or tenant execution.
  • The SOC Investigation Workbench remains development-only pending its complete source package.
  • Host manifests are structurally validated; live installation in each host remains future validation.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-21T19:32:21.009041Z 352c69d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sodejm
sodejm merged commit 380b5ff into main Sep 21, 2026
7 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 352c69d1f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +361 to +365
lowered = content.lower()
for function in profile["unsupported_functions"]:
_require(
f"{str(function).lower()}(" not in lowered,
f"{context}: unsupported function {function}",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject unknown KQL fields and operators

When authored KQL contains an unknown field or invalid operator, validate_hunt() still succeeds because this check only rejects a small list of unsupported functions; for example, appending | extend X = DefinitelyNotARealField or | definitely_not_a_kql_operator 123 is accepted. This contradicts specs/sentinel-hunt-workbench.spec.md:28-29 and lets package checks and release-report static gates pass for KQL that cannot compile, so validate the query against the declared profile/operator grammar or stop reporting that gate as passed.

AGENTS.md reference: AGENTS.md:L156-L157

Useful? React with 👍 / 👎.

Comment on lines +121 to +125
if type_name == "duration":
text = _string(value, name, 16)
if not _DURATION.fullmatch(text):
raise ContentError(f"parameter {name} must be a bounded KQL duration such as 30m or 2h")
return text

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce a real upper bound on KQL durations

When a caller supplies a value such as 999999d, this branch accepts and renders it as a supposedly bounded correlation_window; the regex limits only digit count, not elapsed time. If an analyst executes the rendered hunt, such a window can create extremely broad joins and excessive query cost, contrary to the package's bounded-parameter contract, so parse the unit and enforce an explicit maximum duration.

AGENTS.md reference: AGENTS.md:L101-L102

Useful? React with 👍 / 👎.

Comment on lines +736 to +737
if stage.get("aad_tenant_field"):
lines.append(f"| where tostring({stage['aad_tenant_field']}) == tostring(tenant_id)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Filter source rows by the requested tenant

In a workspace containing telemetry for more than one tenant, most stages never compare the source table's TenantId with the requested tenant_id; instead, every row is stamped with the same constant TenantScope, so the later join cannot prevent cross-tenant correlations. All tables in the shipped Sentinel Analytics profile declare TenantId, but this filter runs only for the few stages with an aad_tenant_field, allowing unrelated tenant events to be reported as one scoped hunt result.

AGENTS.md reference: plugins/AGENTS.md:L9-L13

Useful? React with 👍 / 👎.

Comment on lines +658 to +659
_stage("target_logon", "DeviceLogonEvents", "TimeGenerated", "target_device", "tostring(DeviceId)", ["AccountObjectId", "DeviceId", "LogonId", "ReportId"], ["DeviceId", "ReportId", "TimeGenerated"], join_in="tostring(AccountObjectId)", join_out="tostring(AccountObjectId)", predicate="isnotempty(DeviceId)", evidence="target-device logon"),
_stage("remote_execution", "DeviceProcessEvents", "TimeGenerated", "process", "tostring(ProcessUniqueId)", ["AccountObjectId", "DeviceId", "ProcessUniqueId", "ReportId"], ["DeviceId", "ReportId", "TimeGenerated"], join_in="tostring(AccountObjectId)", join_out="tostring(ProcessUniqueId)", predicate="isnotempty(ProcessUniqueId)", evidence="process execution"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the target device in the H11 correlation

When the same account logs on to multiple devices, H11 joins the target logon to process execution only by AccountObjectId, so a logon on device A can be correlated with a process on device B and presented as lateral movement on the target. The hunt hypothesis explicitly requires execution on the logged-on target device; carry DeviceId through this hop and join on both account and device to avoid unsupported correlations.

AGENTS.md reference: plugins/AGENTS.md:L9-L13

Useful? React with 👍 / 👎.

Comment on lines +577 to +578
_stage("dns_resolution", "DnsEvents", "TimeGenerated", "domain", "tostring(Name)", ["Name", "Computer"], ["Name", "Computer", "TimeGenerated"], join_in="tostring(Name)", join_out="tostring(Computer)", predicate="isnotempty(Name)", evidence="DNS resolution history"),
_stage("originating_process", "DeviceProcessEvents", "TimeGenerated", "device", "tostring(DeviceId)", ["DeviceId", "ProcessUniqueId", "FileName", "ReportId"], ["DeviceId", "ReportId", "TimeGenerated"], join_in="tostring(DeviceId)", predicate="isnotempty(ProcessUniqueId)", evidence="candidate originating process"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Join H06 on compatible device identifiers

For H06, the DNS stage emits DnsEvents.Computer as Join2, while the process stage consumes DeviceProcessEvents.DeviceId; these represent a device name and an immutable Defender device ID respectively, so normal records will not join and the advertised process-attribution hunt will miss matches. Use DeviceName on the process side or add an explicit, validated name-to-ID mapping before this correlation.

AGENTS.md reference: plugins/AGENTS.md:L9-L13

Useful? React with 👍 / 👎.

Comment on lines +20 to +22
"$PYTHON",
"plugins/logging-telemetry/security-logging-advisor/skills/repository-context/scripts/collect-repository-context.py",
"plugins/logging-telemetry/security-logging-advisor"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Redact the checkout path from the logging demo

When users run the newly advertised python3 -m cops demo security-logging-advisor, this command invokes the collector with the package path and the resulting JSON includes repository_path as the absolute checkout path via os.path.abspath. That evidence is intended for analyst or model review and can disclose usernames and machine-specific directory structure when shared, so emit a repository-relative/redacted label instead of the absolute path.

AGENTS.md reference: AGENTS.md:L23-L26

Useful? React with 👍 / 👎.

Comment on lines +540 to +542
def _remove_markers(content: str, markers: tuple[str, ...]) -> str:
lines = [line for line in content.splitlines() if not any(marker in line for marker in markers)]
return "\n".join(lines) + ("\n" if content.endswith("\n") else "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Mutate executable KQL instead of marker comments

Every purported semantic mutation removes only a // huntwb:... marker line, leaving the executable KQL unchanged; validate_hunt() then catches the missing required comment and the report records the mutation as detected. Consequently the advertised 144/144 critical-mutation result does not show that removing time filters, weakening joins, or changing ordering would be detected, so each mutation must alter the corresponding executable clause before calculating the mutation score.

AGENTS.md reference: plugins/AGENTS.md:L9-L13

Useful? React with 👍 / 👎.

"objective": "Correlate spray-like authentication evidence to later account activity without treating shared egress or a successful sign-in as proof of compromise.",
"entities": ["account", "source_ip", "cloud_resource"],
"stages": [
_stage("authentication_failures", "SigninLogs", "TimeGenerated", "account", "tostring(UserId)", ["UserId", "ResultType", "IPAddress", "Id"], ["Id"], join_out="tostring(UserId)", aad_tenant_field="AADTenantId", predicate='ResultType != "0"', evidence="failed authentication"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require actual spray evidence in H01

H01's first stage accepts any single failed sign-in and immediately joins it to later activity by UserId; it never groups failures by source IP, counts distinct targeted accounts, or applies a spray threshold. A user mistyping one password before a successful sign-in can therefore satisfy the advertised password-spray chain, while genuinely distributed spray evidence is not established, so aggregate the failure stage into a bounded multi-account/source pattern before correlating downstream activity.

AGENTS.md reference: plugins/AGENTS.md:L9-L13

Useful? React with 👍 / 👎.

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.

1 participant