Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
52ead71
Replace workflow/shell automation with a tested pluginctl CLI
sethwv Jul 22, 2026
1f63f87
Run pluginctl test suite on tooling changes and gate repo-update PRs …
sethwv Jul 22, 2026
29e1f14
Add regression tests for zero-padded version segments
sethwv Jul 25, 2026
74bc670
Simplify pluginctl: dedupe git/identity/hashing/timestamp helpers, tr…
sethwv Jul 25, 2026
cf70344
Close two PR-validation trust-boundary gaps found in security review
sethwv Jul 25, 2026
3e61c25
feat(ci): respect CodeQL suppressions and stop truncating findings
sethwv Aug 2, 2026
bc78f61
fix(ci): apply CodeQL alert-suppression fix (PR #225/#226) to pluginctl
sethwv Aug 12, 2026
810fea3
fix(ci): prevent script injection via PR title in validate-title
sethwv Aug 12, 2026
d98413e
feat(security): add dormant sandbox bypass detection
sethwv Aug 17, 2026
d3bd7bc
fix(ci): invoke CodeQL CLI from init output
sethwv Aug 22, 2026
d53c39c
feat(publish): gate split manifest base URLs
sethwv Aug 22, 2026
0b12e20
feat(ci): optimize metadata-only plugin validation
sethwv Aug 22, 2026
9c5c7e9
feat(manifest): disclose AI-assisted plugins
sethwv Aug 22, 2026
fe5b4af
feat(security): rebuild plugin sandbox CodeQL checks
sethwv Sep 8, 2026
f56d586
chore(manifest): disable experimental AI disclosure
sethwv Oct 12, 2026
34861cc
feat(security): expand plugin sandbox detection
sethwv Oct 12, 2026
a858e13
feat(automerge): add blacklist and rate limits
sethwv Oct 12, 2026
71d4161
fix(ci): bootstrap trusted checkout action
sethwv Oct 13, 2026
ed3cf45
fix(codeql): expose trusted local query pack
sethwv Oct 13, 2026
475154e
fix(codeql): run trusted local query pack
sethwv Oct 13, 2026
6ae8455
fix(codeql): resolve local query directory
sethwv Oct 13, 2026
41d2e71
fix(codeql): compile sys modules query
sethwv Oct 13, 2026
b0a186f
fix(codeql): use assignment target API
sethwv Oct 13, 2026
85449da
fix(codeql): compile builtins mutation query
sethwv Oct 13, 2026
7832def
test(codeql): compile every custom query
sethwv Oct 13, 2026
88d367a
fix(report): exclude custom findings from CodeQL severity totals
sethwv Oct 13, 2026
6e27c1a
fix(report): simplify maintainer review notices
sethwv Oct 13, 2026
7ef7bbf
fix(security): cover dynamic plugin bypass patterns
sethwv Oct 13, 2026
a02a7e5
test(ci): run CodeQL query suite for repo updates
sethwv Oct 13, 2026
cb88523
refactor(codeql): simplify capability alert wording
sethwv Oct 13, 2026
ce1d9c0
test(pluginctl): adapt SARIF coverage to feature flags
sethwv Oct 14, 2026
f02ce16
test(pluginctl): isolate label catalog environment
sethwv Oct 14, 2026
78d8950
feat(discord): add Components V2 lifecycle notifications
sethwv Oct 14, 2026
35d753d
temp enable feature flags
sethwv Oct 14, 2026
0d0b7f3
fix(discord): enrich review and release card links
sethwv Oct 14, 2026
ca301c2
fix(automerge): notify for every blocked merge
sethwv Oct 14, 2026
b6275c7
style(labels): use gray for manual review
sethwv Oct 14, 2026
bd03a4f
fix(automerge): enrich disabled merge review cards
sethwv Oct 14, 2026
61ed675
fix(links): use registry tags for local plugins
sethwv Oct 14, 2026
a50041f
fix(codeql): allow Actions API reads
sethwv Oct 14, 2026
4bc917b
fix(workflows): use app identity for label updates
sethwv Oct 14, 2026
0eda71b
fix(clamav): include trusted token action
sethwv Oct 14, 2026
fe61580
fix(releases): link local plugins to squash commits
sethwv Oct 15, 2026
4370778
revert feature flag enablement
sethwv Oct 15, 2026
26efea3
docs(policy): define plugin repository guidelines
sethwv Oct 15, 2026
c18358b
docs(policy): refine repository rules
sethwv Oct 15, 2026
c649964
docs(policy): clarify pull request requirements
sethwv Oct 15, 2026
e6d250c
docs(policy): clarify repository contribution rules
sethwv Oct 15, 2026
b5aa351
docs(maintainers): document repository operations
sethwv Oct 15, 2026
6c6a7d2
docs(contributing): explain automatic merge limits
sethwv Oct 15, 2026
5443f7d
docs(workflows): add workflow transition guidance
sethwv Oct 15, 2026
627e3c3
docs(coc): add workflow transition notice
sethwv Oct 15, 2026
c79807c
docs(contributing): simplify guide title
sethwv Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/ISSUE_TEMPLATE/repo-issue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ body:
- type: markdown
attributes:
value: |
> **Workflow transition**
> The repository's workflows recently changed. Please bear with us while we work through expected issues and hiccups.

> **Plugin issues belong elsewhere.**
> If your issue is about a specific plugin (bugs, feature requests, compatibility), use one of these instead:
> - The plugin's own GitHub repository (linked in its `plugin.json`)
Expand Down
8 changes: 7 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@

## About this submission

<!-- Briefly describe the change: new plugin, update, metadata change, etc. -->
<!--
Briefly describe the change: new plugin, update, metadata change, etc.

Use of this template is optional, but every pull request must include a meaningful description. A blank or materially incomplete description may result in auto-merge privileges being revoked.

If submitting through automation, consider including a link to the relevant changelog, release notes, or other change summary.
-->

## Pre-submission checklist

Expand Down
85 changes: 85 additions & 0 deletions .github/actions/gh-app-token/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: GitHub App token
description: >-
Mint a GitHub App installation token when GH_APP_ID + GH_APP_PRIVATE_KEY are
configured, otherwise fall back to the workflow GITHUB_TOKEN
(github-actions[bot]) with the same step-summary warning. Replaces the token
block that was copy-pasted across the plugin workflows.

inputs:
app-id:
description: GitHub App client/app id (pass vars.GH_APP_ID)
required: false
default: ""
private-key:
description: >-
GitHub App private key (pass secrets.GH_APP_PRIVATE_KEY). Composite actions
do not inherit secrets, so it must be provided as an input.
required: false
default: ""
owner:
description: Optional owner to scope the installation token to.
required: false
default: ""

outputs:
token:
description: The App installation token, or the fallback github.token.
value: ${{ steps.result.outputs.token }}
app-slug:
description: The App slug (empty when falling back).
value: ${{ steps.app-token.outputs.app-slug }}
use_app:
description: "'true' when an App token was minted, 'false' on fallback."
value: ${{ steps.config.outputs.use_app }}
bot-login:
description: The bot login to attribute actions to.
value: ${{ steps.result.outputs.bot_login }}

runs:
using: composite
steps:
- name: Resolve configuration
id: config
shell: bash
env:
GH_APP_ID: ${{ inputs.app-id }}
GH_APP_PRIVATE_KEY: ${{ inputs.private-key }}
run: |
if [[ -n "${GH_APP_ID:-}" && -n "${GH_APP_PRIVATE_KEY:-}" ]]; then
echo "app_id=${GH_APP_ID}" >> "$GITHUB_OUTPUT"
echo "use_app=true" >> "$GITHUB_OUTPUT"
else
echo "use_app=false" >> "$GITHUB_OUTPUT"
fi

- name: Generate GitHub App token
if: steps.config.outputs.use_app == 'true'
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ steps.config.outputs.app_id }}
private-key: ${{ inputs.private-key }}
owner: ${{ inputs.owner }}

- name: Log fallback to actions token
if: steps.config.outputs.use_app != 'true'
shell: bash
run: |
printf '## ⚠️ GitHub App token not available\n\nGH_APP_ID or GH_APP_PRIVATE_KEY not configured. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY"

- name: Expose token + bot login
id: result
shell: bash
env:
USE_APP: ${{ steps.config.outputs.use_app }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
FALLBACK_TOKEN: ${{ github.token }}
run: |
if [[ "$USE_APP" == "true" ]]; then
echo "token=$APP_TOKEN" >> "$GITHUB_OUTPUT"
echo "bot_login=${APP_SLUG}[bot]" >> "$GITHUB_OUTPUT"
else
echo "token=$FALLBACK_TOKEN" >> "$GITHUB_OUTPUT"
echo "bot_login=github-actions[bot]" >> "$GITHUB_OUTPUT"
fi
64 changes: 64 additions & 0 deletions .github/actions/trusted-checkout/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Trusted checkout
description: >-
The pull_request_target checkout "dance": check out the base branch's trusted
code, sparse-checkout the untrusted fork's plugins/ as data only, then restore
the trusted code and re-fetch the base refs. Fork content is never executed;
pluginctl always comes from the base branch. Replaces the sequence duplicated
across detect-changes / validate-plugin.

inputs:
base-ref:
description: PR base branch (github.event.pull_request.base.ref)
required: true
head-repo:
description: Fork repo full name (github.event.pull_request.head.repo.full_name)
required: true
head-sha:
description: Fork head SHA (github.event.pull_request.head.sha)
required: true
repository:
description: This repository (github.repository)
required: true

runs:
using: composite
steps:
- name: Checkout base branch code (trusted)
uses: actions/checkout@v6
with:
repository: ${{ inputs.repository }}
ref: ${{ inputs.base-ref }}
fetch-depth: 0
sparse-checkout: .github
sparse-checkout-cone-mode: false

- name: Fetch base branch
shell: bash
run: git fetch origin ${{ inputs.base-ref }}

- name: Save trusted code before fork checkout
shell: bash
run: cp -r .github /tmp/trusted-dotgithub

- name: Checkout PR plugins (untrusted content only)
uses: actions/checkout@v6
with:
repository: ${{ inputs.head-repo }}
ref: ${{ inputs.head-sha }}
fetch-depth: 0
sparse-checkout: plugins
sparse-checkout-cone-mode: false
clean: false
# Fork content is only read as data (sparse plugins/), never executed;
# trusted code is saved/restored around this step.
allow-unsafe-pr-checkout: true

- name: Restore trusted code
shell: bash
run: |
rm -rf .github
cp -r /tmp/trusted-dotgithub .github

- name: Re-fetch base branch refs
shell: bash
run: git fetch https://github.com/${{ inputs.repository }} +${{ inputs.base-ref }}:refs/remotes/origin/${{ inputs.base-ref }}
6 changes: 5 additions & 1 deletion .github/codeql/codeql-config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Extra CodeQL query applied alongside the `security-extended` suite used by
# validate-plugin.yml. `packs:` is required here (rather than the simpler
# _codeql-scan.yml. `packs:` is required here (rather than the simpler
# `packs:` input on codeql-action/init) because that input only works for
# single-language analyses, and our workflow can initialize multiple
# languages at once.
Expand All @@ -14,6 +14,7 @@
# comments are ignored.
queries:
- uses: security-extended
- uses: ./.github/codeql/queries/python
packs:
python:
- codeql/python-queries:AlertSuppression.ql
Expand All @@ -27,3 +28,6 @@ packs:
- codeql/java-queries:AlertSuppression.ql
cpp:
- codeql/cpp-queries:AlertSuppression.ql
paths-ignore:
- .github/codeql/**
- _trusted/**
18 changes: 18 additions & 0 deletions .github/codeql/queries/python/AsyncioProcessApi.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @name Plugin asyncio process API bypass
* @description asyncio subprocess APIs bypass Dispatcharr's guarded subprocess module.
* @kind problem
* @problem.severity warning
* @precision high
* @id plugin/sandbox-bypass/asyncio-process-api
* @tags security external/cwe/cwe-693
*/

import python
import semmle.python.ApiGraphs

from API::CallNode call
where
call = API::moduleImport("asyncio")
.getMember(["create_subprocess_exec", "create_subprocess_shell"]).getACall()
select call, "This asyncio subprocess API bypasses Dispatcharr's guarded subprocess module."
20 changes: 20 additions & 0 deletions .github/codeql/queries/python/BackgroundTasksUsage.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @name Plugin background task capability use
* @description Dispatcharr's plugin context dispatcher requires the background_tasks capability.
* @kind problem
* @problem.severity warning
* @precision medium
* @id plugin/capability-contract/background-tasks
* @tags security external/cwe/cwe-269
*/

import python

from Call call, Subscript helper, StringLiteral key, Name context
where
call.getFunc() = helper and
helper.getObject() = context and
context.getId() = "context" and
helper.getIndex() = key and
key.getText() = "dispatch_task"
select call, "This plugin task dispatch requires the `background_tasks` capability."
34 changes: 34 additions & 0 deletions .github/codeql/queries/python/BuiltinsMutation.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @name Plugin mutation of builtins
* @description Mutating the plugin's builtins mapping can replace its guarded import or open hooks.
* @kind problem
* @problem.severity warning
* @precision high
* @id plugin/sandbox-bypass/builtins-mutation
* @tags security external/cwe/cwe-693
*/

import python

predicate builtinsObject(Expr expression) {
exists(Name builtins | expression = builtins and builtins.getId() = "__builtins__")
}

from Expr candidate
where
exists(AssignStmt assignment, Expr target |
assignment.getATarget() = target and
(
exists(Attribute attribute | target = attribute and builtinsObject(attribute.getObject())) or
exists(Subscript subscript | target = subscript and builtinsObject(subscript.getObject()))
) and
candidate = target
)
or
exists(Call call, Name setattr |
call.getFunc() = setattr and
setattr.getId() = "setattr" and
builtinsObject(call.getArg(0)) and
candidate = call
)
select candidate, "Mutating __builtins__ can replace Dispatcharr's plugin-local guarded import or open hooks."
20 changes: 20 additions & 0 deletions .github/codeql/queries/python/CeleryDispatchUsage.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @name Plugin internal task dispatch capability use
* @description Dispatcharr's allowlisted internal-task dispatcher requires the celery_dispatch capability.
* @kind problem
* @problem.severity warning
* @precision medium
* @id plugin/capability-contract/celery-dispatch
* @tags security external/cwe/cwe-269
*/

import python

from Call call, Subscript helper, StringLiteral key, Name context
where
call.getFunc() = helper and
helper.getObject() = context and
context.getId() = "context" and
helper.getIndex() = key and
key.getText() = "dispatch_internal_task"
select call, "This internal task dispatch requires the `celery_dispatch` capability."
15 changes: 15 additions & 0 deletions .github/codeql/queries/python/CtypesUsage.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @name Plugin use of ctypes
* @description Native ctypes access can execute outside Dispatcharr's plugin-local wrappers.
* @kind problem
* @problem.severity warning
* @precision high
* @id plugin/sandbox-bypass/ctypes-usage
* @tags security external/cwe/cwe-693
*/

import python

from Import imp
where imp.getAnImportedModuleName() = "ctypes"
select imp, "ctypes can load native code outside Dispatcharr's plugin capability wrappers."
60 changes: 60 additions & 0 deletions .github/codeql/queries/python/DirectBuiltinsAccess.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @name Plugin direct access to original builtins
* @description Accessing the global builtins module can bypass the plugin-local open and import wrappers.
* @kind problem
* @problem.severity warning
* @precision high
* @id plugin/sandbox-bypass/direct-builtins-access
* @tags security external/cwe/cwe-693
*/

import python
import semmle.python.dataflow.new.DataFlow

predicate constantString(Expr expression, string value) {
exists(StringLiteral literal |
expression = literal and value = literal.getText()
)
or
exists(BinaryExpr addition, Expr left, Expr right, string leftValue, string rightValue |
expression = addition and
addition.getLeft() = left and
addition.getRight() = right and
constantString(left, leftValue) and
constantString(right, rightValue) and
value = leftValue + rightValue
)
}

predicate dynamicBuiltinsAccess(Call access) {
exists(Call importCall, Name importFunction, Expr moduleName, Expr moduleNameSource,
Call memberLookup, Name memberLookupFunction, Expr importedModule,
Expr memberName, Expr memberNameSource, string memberValue |
importCall.getFunc() = importFunction and importFunction.getId() = "__import__" and
importCall.getArg(0) = moduleName and
constantString(moduleNameSource, "builtins") and
DataFlow::localFlow(DataFlow::exprNode(moduleNameSource), DataFlow::exprNode(moduleName)) and
memberLookup.getFunc() = memberLookupFunction and memberLookupFunction.getId() = "getattr" and
memberLookup.getArg(0) = importedModule and
DataFlow::localFlow(DataFlow::exprNode(importCall), DataFlow::exprNode(importedModule)) and
memberLookup.getArg(1) = memberName and
constantString(memberNameSource, memberValue) and
DataFlow::localFlow(DataFlow::exprNode(memberNameSource), DataFlow::exprNode(memberName)) and
memberValue in ["open", "__import__"] and
access = memberLookup
)
}

from Call call
where
(
exists(Attribute function, Name mod |
call.getFunc() = function and
function.getObject() = mod and
mod.getId() = "builtins" and
function.getName() in ["open", "__import__"]
)
or
dynamicBuiltinsAccess(call)
)
select call, "Accessing the original builtins module bypasses Dispatcharr's plugin-local wrappers."
25 changes: 25 additions & 0 deletions .github/codeql/queries/python/FilesystemApiBypass.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @name Plugin direct filesystem API bypass
* @description Direct filesystem APIs bypass Dispatcharr's plugin-local builtins.open wrapper.
* @kind problem
* @problem.severity warning
* @precision medium
* @id plugin/sandbox-bypass/filesystem-api
* @tags security external/cwe/cwe-693
*/

import python
import semmle.python.ApiGraphs

from API::CallNode call
where
(
call = API::moduleImport("io").getMember("open").getACall() or
call = API::moduleImport("os").getMember("open").getACall() or
call = API::moduleImport("shutil")
.getMember(["copy", "copy2", "copyfile", "copytree", "move", "rmtree"]).getACall() or
call = API::moduleImport("pathlib").getMember("Path").getACall().getReturn()
.getMember(["open", "write_text", "write_bytes", "touch", "mkdir", "unlink", "rename", "replace"])
.getACall()
)
select call, "This direct filesystem API bypasses Dispatcharr's plugin-local open wrapper."
Loading
Loading