ci: implement enterprise-grade PR quality gates and security scanning (#266)

* ci: implement enterprise-grade PR quality gates and security scanning

* ci: implement enterprise-grade PR quality gates and security scanning

* fix:pr comments and improve code

* fix: improve commit linting and code quality

* Removed the dependency-review job (i added it)

* fix: address CodeRabbit review comments

- Expand scope pattern to allow uppercase, underscores, slashes, dots
- Add concurrency control to cancel duplicate security scan runs
- Add explanatory comment for Bandit CLI flags
- Remove dependency-review job (requires repo settings)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: update commit lint examples with expanded scope patterns

Show slashes and dots in scope examples to demonstrate
the newly allowed characters (api/users, package.json)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: remove feature request issue template

Feature requests are directed to GitHub Discussions
via the issue template config.yml

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address security vulnerabilities in service orchestrator

- Fix port parsing crash on malformed docker-compose entries
- Fix shell injection risk by using shlex.split() with shell=False

Prevents crashes when docker-compose.yml contains environment
variables in port mappings (e.g., '${PORT}:8080') and eliminates
shell injection vulnerabilities in subprocess execution.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex
2025-12-25 15:51:55 +01:00
committed by GitHub
parent a3f87540c6
commit d42041c5b5
16 changed files with 855 additions and 35 deletions
+37
View File
@@ -0,0 +1,37 @@
version: 2
updates:
# Python dependencies
- package-ecosystem: pip
directory: /apps/backend
schedule:
interval: weekly
open-pull-requests-limit: 5
labels:
- dependencies
- python
commit-message:
prefix: "chore(deps)"
# npm dependencies
- package-ecosystem: npm
directory: /apps/frontend
schedule:
interval: weekly
open-pull-requests-limit: 5
labels:
- dependencies
- javascript
commit-message:
prefix: "chore(deps)"
# GitHub Actions
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
labels:
- dependencies
- ci
commit-message:
prefix: "ci(deps)"
+1 -1
View File
@@ -50,7 +50,7 @@ jobs:
PYTHONPATH: ${{ github.workspace }}/apps/backend
run: |
source .venv/bin/activate
pytest ../../tests/ -v --cov=. --cov-report=xml --cov-report=term-missing
pytest ../../tests/ -v --cov=. --cov-report=xml --cov-report=term-missing --cov-fail-under=30
- name: Upload coverage reports
if: matrix.python-version == '3.12'
@@ -1,4 +1,4 @@
name: Auto Label
name: Issue Auto Label
on:
issues:
-23
View File
@@ -28,26 +28,3 @@ jobs:
- name: Run ruff format check
run: ruff format apps/backend/ --check --diff
# TypeScript/React linting
frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Install dependencies
working-directory: apps/frontend
run: npm ci --ignore-scripts
- name: Run ESLint
working-directory: apps/frontend
run: npm run lint
- name: Run TypeScript check
working-directory: apps/frontend
run: npm run typecheck
+227
View File
@@ -0,0 +1,227 @@
name: PR Auto Label
on:
pull_request:
types: [opened, synchronize, reopened]
# Cancel in-progress runs for the same PR
concurrency:
group: pr-auto-label-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
jobs:
label:
name: Auto Label PR
runs-on: ubuntu-latest
# Don't run on fork PRs (they can't write labels)
if: github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 5
steps:
- name: Auto-label PR
uses: actions/github-script@v7
with:
retries: 3
retry-exempt-status-codes: 400,401,403,404,422
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const prNumber = pr.number;
const title = pr.title;
console.log(`::group::PR #${prNumber} - Auto-labeling`);
console.log(`Title: ${title}`);
const labelsToAdd = new Set();
const labelsToRemove = new Set();
// ═══════════════════════════════════════════════════════════════
// TYPE LABELS (from PR title - Conventional Commits)
// ═══════════════════════════════════════════════════════════════
const typeMap = {
'feat': 'feature',
'fix': 'bug',
'docs': 'documentation',
'refactor': 'refactor',
'test': 'test',
'ci': 'ci',
'chore': 'chore',
'perf': 'performance',
'style': 'style',
'build': 'build'
};
const typeMatch = title.match(/^(\w+)(\(.+?\))?(!)?:/);
if (typeMatch) {
const type = typeMatch[1].toLowerCase();
const isBreaking = typeMatch[3] === '!';
if (typeMap[type]) {
labelsToAdd.add(typeMap[type]);
console.log(` 📝 Type: ${type} → ${typeMap[type]}`);
}
if (isBreaking) {
labelsToAdd.add('breaking-change');
console.log(` ⚠️ Breaking change detected`);
}
} else {
console.log(` ⚠️ No conventional commit prefix found in title`);
}
// ═══════════════════════════════════════════════════════════════
// AREA LABELS (from changed files)
// ═══════════════════════════════════════════════════════════════
let files = [];
try {
const { data } = await github.rest.pulls.listFiles({
owner,
repo,
pull_number: prNumber,
per_page: 100
});
files = data;
} catch (e) {
console.log(` ⚠️ Could not fetch files: ${e.message}`);
}
const areas = {
frontend: false,
backend: false,
ci: false,
docs: false,
tests: false
};
for (const file of files) {
const path = file.filename;
if (path.startsWith('apps/frontend/')) areas.frontend = true;
if (path.startsWith('apps/backend/')) areas.backend = true;
if (path.startsWith('.github/')) areas.ci = true;
if (path.endsWith('.md') || path.startsWith('docs/')) areas.docs = true;
if (path.startsWith('tests/') || path.includes('.test.') || path.includes('.spec.')) areas.tests = true;
}
// Determine area label (mutually exclusive)
const areaLabels = ['area/frontend', 'area/backend', 'area/fullstack', 'area/ci'];
if (areas.frontend && areas.backend) {
labelsToAdd.add('area/fullstack');
areaLabels.filter(l => l !== 'area/fullstack').forEach(l => labelsToRemove.add(l));
console.log(` 📁 Area: fullstack (${files.length} files)`);
} else if (areas.frontend) {
labelsToAdd.add('area/frontend');
areaLabels.filter(l => l !== 'area/frontend').forEach(l => labelsToRemove.add(l));
console.log(` 📁 Area: frontend (${files.length} files)`);
} else if (areas.backend) {
labelsToAdd.add('area/backend');
areaLabels.filter(l => l !== 'area/backend').forEach(l => labelsToRemove.add(l));
console.log(` 📁 Area: backend (${files.length} files)`);
} else if (areas.ci) {
labelsToAdd.add('area/ci');
areaLabels.filter(l => l !== 'area/ci').forEach(l => labelsToRemove.add(l));
console.log(` 📁 Area: ci (${files.length} files)`);
}
// ═══════════════════════════════════════════════════════════════
// SIZE LABELS (from lines changed)
// ═══════════════════════════════════════════════════════════════
const additions = pr.additions || 0;
const deletions = pr.deletions || 0;
const totalLines = additions + deletions;
const sizeLabels = ['size/XS', 'size/S', 'size/M', 'size/L', 'size/XL'];
let sizeLabel;
if (totalLines < 10) sizeLabel = 'size/XS';
else if (totalLines < 100) sizeLabel = 'size/S';
else if (totalLines < 500) sizeLabel = 'size/M';
else if (totalLines < 1000) sizeLabel = 'size/L';
else sizeLabel = 'size/XL';
labelsToAdd.add(sizeLabel);
sizeLabels.filter(l => l !== sizeLabel).forEach(l => labelsToRemove.add(l));
console.log(` 📏 Size: ${sizeLabel} (+${additions}/-${deletions} = ${totalLines} lines)`);
console.log('::endgroup::');
// ═══════════════════════════════════════════════════════════════
// APPLY LABELS
// ═══════════════════════════════════════════════════════════════
console.log(`::group::Applying labels`);
// Remove old labels (in parallel)
const removeArray = [...labelsToRemove].filter(l => !labelsToAdd.has(l));
if (removeArray.length > 0) {
const removePromises = removeArray.map(async (label) => {
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: label
});
console.log(` ✓ Removed: ${label}`);
} catch (e) {
if (e.status !== 404) {
console.log(` ⚠ Could not remove ${label}: ${e.message}`);
}
}
});
await Promise.all(removePromises);
}
// Add new labels
const addArray = [...labelsToAdd];
if (addArray.length > 0) {
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: addArray
});
console.log(` ✓ Added: ${addArray.join(', ')}`);
} catch (e) {
// Some labels might not exist
if (e.status === 404) {
core.warning(`Some labels do not exist. Please create them in repository settings.`);
// Try adding one by one
for (const label of addArray) {
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [label]
});
} catch (e2) {
console.log(` ⚠ Label '${label}' does not exist`);
}
}
} else {
throw e;
}
}
}
console.log('::endgroup::');
// Summary
console.log(`✅ PR #${prNumber} labeled: ${addArray.join(', ')}`);
// Write job summary
core.summary
.addHeading(`PR #${prNumber} Auto-Labels`, 3)
.addTable([
[{data: 'Category', header: true}, {data: 'Label', header: true}],
['Type', typeMatch ? typeMap[typeMatch[1].toLowerCase()] || 'none' : 'none'],
['Area', areas.frontend && areas.backend ? 'fullstack' : areas.frontend ? 'frontend' : areas.backend ? 'backend' : 'other'],
['Size', sizeLabel]
])
.addRaw(`\n**Files changed:** ${files.length}\n`)
.addRaw(`**Lines:** +${additions} / -${deletions}\n`);
await core.summary.write();
+72
View File
@@ -0,0 +1,72 @@
name: PR Status Check
on:
pull_request:
types: [opened, synchronize, reopened]
# Cancel in-progress runs for the same PR
concurrency:
group: pr-status-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
pull-requests: write
jobs:
mark-checking:
name: Set Checking Status
runs-on: ubuntu-latest
# Don't run on fork PRs (they can't write labels)
if: github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 5
steps:
- name: Update PR status label
uses: actions/github-script@v7
with:
retries: 3
retry-exempt-status-codes: 400,401,403,404,422
script: |
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request.number;
const statusLabels = ['🔄 Checking', '✅ Ready for Review', '❌ Checks Failed'];
console.log(`::group::PR #${prNumber} - Setting status to Checking`);
// Remove old status labels (parallel for speed)
const removePromises = statusLabels.map(async (label) => {
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: label
});
console.log(` ✓ Removed: ${label}`);
} catch (e) {
if (e.status !== 404) {
console.log(` ⚠ Could not remove ${label}: ${e.message}`);
}
}
});
await Promise.all(removePromises);
// Add checking label
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: ['🔄 Checking']
});
console.log(` ✓ Added: 🔄 Checking`);
} catch (e) {
// Label might not exist - create helpful error
if (e.status === 404) {
core.warning(`Label '🔄 Checking' does not exist. Please create it in repository settings.`);
}
throw e;
}
console.log('::endgroup::');
console.log(`✅ PR #${prNumber} marked as checking`);
+161
View File
@@ -0,0 +1,161 @@
name: PR Status Gate
on:
workflow_run:
workflows: [CI, Lint, Quality Security, Quality DCO, Quality Commit Lint]
types: [completed]
permissions:
pull-requests: write
actions: read
jobs:
update-status:
name: Update PR Status
runs-on: ubuntu-latest
# Only run if this workflow_run is associated with a PR
if: github.event.workflow_run.pull_requests[0] != null
timeout-minutes: 5
steps:
- name: Check all workflows and update label
uses: actions/github-script@v7
with:
retries: 3
retry-exempt-status-codes: 400,401,403,404,422
script: |
const { owner, repo } = context.repo;
const prNumber = context.payload.workflow_run.pull_requests[0].number;
const headSha = context.payload.workflow_run.head_sha;
const triggerWorkflow = context.payload.workflow_run.name;
// Required workflows configuration
const required = [
{ name: 'CI', file: 'ci.yml' },
{ name: 'Lint', file: 'lint.yml' },
{ name: 'Quality Security', file: 'quality-security.yml' },
{ name: 'Quality DCO', file: 'quality-dco.yml' },
{ name: 'Quality Commit Lint', file: 'quality-commit-lint.yml' }
];
const statusLabels = {
checking: '🔄 Checking',
passed: '✅ Ready for Review',
failed: '❌ Checks Failed'
};
console.log(`::group::PR #${prNumber} - Checking workflow status`);
console.log(`Triggered by: ${triggerWorkflow}`);
console.log(`Head SHA: ${headSha}`);
console.log('');
let allComplete = true;
let anyFailed = false;
const results = [];
// Check all required workflows
for (const workflow of required) {
try {
const { data } = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: workflow.file,
head_sha: headSha,
per_page: 1
});
if (!data.workflow_runs.length) {
results.push({ name: workflow.name, status: '⏳ Pending', complete: false });
allComplete = false;
continue;
}
const run = data.workflow_runs[0];
if (run.status !== 'completed') {
results.push({ name: workflow.name, status: '🔄 Running', complete: false });
allComplete = false;
} else if (run.conclusion === 'success') {
results.push({ name: workflow.name, status: '✅ Passed', complete: true });
} else if (run.conclusion === 'skipped') {
results.push({ name: workflow.name, status: '⏭️ Skipped', complete: true });
} else {
results.push({ name: workflow.name, status: '❌ Failed', complete: true, failed: true });
anyFailed = true;
}
} catch (error) {
console.log(`::warning::Error checking ${workflow.name}: ${error.message}`);
results.push({ name: workflow.name, status: '⚠️ Error', complete: false });
allComplete = false;
}
}
// Print results table
console.log('Workflow Status:');
console.log('─'.repeat(40));
for (const r of results) {
console.log(` ${r.status.padEnd(12)} ${r.name}`);
}
console.log('─'.repeat(40));
console.log('::endgroup::');
// Only update label if all workflows are complete
if (!allComplete) {
console.log('⏳ Not all workflows complete yet - keeping current label');
return;
}
// Determine final label
const newLabel = anyFailed ? statusLabels.failed : statusLabels.passed;
console.log(`::group::Updating PR #${prNumber} label`);
// Remove old status labels
for (const label of Object.values(statusLabels)) {
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: label
});
console.log(` ✓ Removed: ${label}`);
} catch (e) {
if (e.status !== 404) {
console.log(` ⚠ Could not remove ${label}: ${e.message}`);
}
}
}
// Add final status label
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [newLabel]
});
console.log(` ✓ Added: ${newLabel}`);
} catch (e) {
if (e.status === 404) {
core.warning(`Label '${newLabel}' does not exist. Please create it in repository settings.`);
}
throw e;
}
console.log('::endgroup::');
// Summary
if (anyFailed) {
console.log(`❌ PR #${prNumber} has failing checks`);
core.summary.addRaw(`## ❌ PR #${prNumber} - Checks Failed\n\n`);
} else {
console.log(`✅ PR #${prNumber} is ready for review`);
core.summary.addRaw(`## ✅ PR #${prNumber} - Ready for Review\n\n`);
}
// Add results to summary
core.summary.addTable([
[{data: 'Workflow', header: true}, {data: 'Status', header: true}],
...results.map(r => [r.name, r.status])
]);
await core.summary.write();
+74
View File
@@ -0,0 +1,74 @@
name: Quality Commit Lint
on:
pull_request:
branches: [main, develop]
types: [opened, edited, synchronize, reopened]
permissions:
contents: read
pull-requests: read
jobs:
check:
name: Conventional Commits
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Validate PR title
uses: actions/github-script@v7
with:
retries: 3
script: |
const pr = context.payload.pull_request;
const title = pr.title;
console.log(`::group::PR #${pr.number} - Validating PR title`);
console.log(`Title: ${title}`);
// Conventional Commits pattern for PR title
// type(scope)?: description (max 100 chars)
// Optional ! for breaking changes: feat!: or feat(scope)!:
// Scope allows: letters, numbers, hyphens, underscores, slashes, dots
const pattern = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-zA-Z0-9_\-\/\.]+\))?!?: .{1,100}$/;
const isValid = pattern.test(title);
console.log(`Valid: ${isValid}`);
console.log('::endgroup::');
if (!isValid) {
let errorMsg = '## ❌ PR Title Validation Failed\n\n';
errorMsg += `Your PR title does not follow [Conventional Commits](https://www.conventionalcommits.org/) format:\n\n`;
errorMsg += `> \`${title}\`\n\n`;
errorMsg += '### Expected Format\n\n';
errorMsg += '```\ntype(scope): description\n```\n\n';
errorMsg += '| Type | Description |\n';
errorMsg += '|------|-------------|\n';
errorMsg += '| `feat` | New feature |\n';
errorMsg += '| `fix` | Bug fix |\n';
errorMsg += '| `docs` | Documentation only |\n';
errorMsg += '| `style` | Code style (formatting, etc.) |\n';
errorMsg += '| `refactor` | Code refactoring |\n';
errorMsg += '| `perf` | Performance improvement |\n';
errorMsg += '| `test` | Adding/updating tests |\n';
errorMsg += '| `build` | Build system changes |\n';
errorMsg += '| `ci` | CI/CD changes |\n';
errorMsg += '| `chore` | Maintenance tasks |\n';
errorMsg += '| `revert` | Reverting changes |\n\n';
errorMsg += '### Examples\n\n';
errorMsg += '```\nfeat(auth): add OAuth2 login support\nfix(api/users): handle null response correctly\nfix(package.json): update dependencies\ndocs: update README installation steps\nci: add automated release workflow\n```\n\n';
errorMsg += '### How to Fix\n\n';
errorMsg += 'Edit your PR title to follow the format above.\n';
core.summary.addRaw(errorMsg);
await core.summary.write();
core.setFailed('PR title does not follow Conventional Commits format');
} else {
console.log(`✅ PR title follows Conventional Commits format`);
core.summary
.addHeading('✅ PR Title Valid', 3)
.addRaw(`PR title follows Conventional Commits format: \`${title}\``);
await core.summary.write();
}
+70
View File
@@ -0,0 +1,70 @@
name: Quality DCO
on:
pull_request:
branches: [main, develop]
permissions:
contents: read
pull-requests: read
jobs:
check:
name: DCO Check
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check DCO Sign-off
uses: dcoapp/dco-check@v1
with:
require-signoff: true
- name: DCO Help on Failure
if: failure()
uses: actions/github-script@v7
with:
script: |
const helpMsg = `## ❌ DCO Sign-off Required
This project requires all commits to be signed off with the Developer Certificate of Origin (DCO).
### How to Fix
**Option 1: Sign off your last commit**
\`\`\`bash
git commit --amend --signoff
git push --force-with-lease
\`\`\`
**Option 2: Sign off all commits in this PR**
\`\`\`bash
git rebase HEAD~N --signoff # Replace N with number of commits
git push --force-with-lease
\`\`\`
**Option 3: Configure git to always sign off**
\`\`\`bash
git config --global format.signoff true
\`\`\`
### What is DCO?
The [Developer Certificate of Origin](https://developercertificate.org/) is a lightweight way for contributors to certify that they wrote or have the right to submit the code they are contributing.
By signing off, you agree to the DCO terms:
- The contribution was created by you
- You have the right to submit it under the project's license
- You understand the contribution is public and recorded
### Sign-off Format
Your commit message should end with:
\`\`\`
Signed-off-by: Your Name <your.email@example.com>
\`\`\`
This line is automatically added when you use \`git commit -s\` or \`git commit --signoff\`.
`;
core.summary.addRaw(helpMsg);
await core.summary.write();
+178
View File
@@ -0,0 +1,178 @@
name: Quality Security
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
schedule:
- cron: '0 0 * * 1' # Weekly on Monday at midnight UTC
# Cancel in-progress runs for the same branch/PR
concurrency:
group: security-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
security-events: write
actions: read
jobs:
codeql:
name: CodeQL (${{ matrix.language }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
language: [python, javascript-typescript]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: +security-extended,security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
python-security:
name: Python Security (Bandit)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Bandit
run: pip install bandit
- name: Run Bandit security scan
id: bandit
run: |
echo "::group::Running Bandit security scan"
# Run Bandit; exit code 1 means issues found (expected), other codes are errors
# Flags: -r=recursive, -ll=severity LOW+, -ii=confidence LOW+, -f=format, -o=output
bandit -r apps/backend/ -ll -ii -f json -o bandit-report.json || BANDIT_EXIT=$?
if [ "${BANDIT_EXIT:-0}" -gt 1 ]; then
echo "::error::Bandit scan failed with exit code $BANDIT_EXIT"
exit 1
fi
echo "::endgroup::"
- name: Analyze Bandit results
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
// Check if report exists
if (!fs.existsSync('bandit-report.json')) {
core.setFailed('Bandit report not found - scan may have failed');
return;
}
const report = JSON.parse(fs.readFileSync('bandit-report.json', 'utf8'));
const results = report.results || [];
// Categorize by severity
const high = results.filter(r => r.issue_severity === 'HIGH');
const medium = results.filter(r => r.issue_severity === 'MEDIUM');
const low = results.filter(r => r.issue_severity === 'LOW');
console.log(`::group::Bandit Security Scan Results`);
console.log(`Found ${results.length} issues:`);
console.log(` 🔴 HIGH: ${high.length}`);
console.log(` 🟡 MEDIUM: ${medium.length}`);
console.log(` 🟢 LOW: ${low.length}`);
console.log('');
// Print high severity issues
if (high.length > 0) {
console.log('High Severity Issues:');
console.log('─'.repeat(60));
for (const issue of high) {
console.log(` ${issue.filename}:${issue.line_number}`);
console.log(` ${issue.issue_text}`);
console.log(` Test: ${issue.test_id} (${issue.test_name})`);
console.log('');
}
}
console.log('::endgroup::');
// Build summary
let summary = `## 🔒 Python Security Scan (Bandit)\n\n`;
summary += `| Severity | Count |\n`;
summary += `|----------|-------|\n`;
summary += `| 🔴 High | ${high.length} |\n`;
summary += `| 🟡 Medium | ${medium.length} |\n`;
summary += `| 🟢 Low | ${low.length} |\n\n`;
if (high.length > 0) {
summary += `### High Severity Issues\n\n`;
for (const issue of high) {
summary += `- **${issue.filename}:${issue.line_number}**\n`;
summary += ` - ${issue.issue_text}\n`;
summary += ` - Test: \`${issue.test_id}\` (${issue.test_name})\n\n`;
}
}
core.summary.addRaw(summary);
await core.summary.write();
// Fail if high severity issues found
if (high.length > 0) {
core.setFailed(`Found ${high.length} high severity security issue(s)`);
} else {
console.log('✅ No high severity security issues found');
}
# Summary job that waits for all security checks
security-summary:
name: Security Summary
runs-on: ubuntu-latest
needs: [codeql, python-security]
if: always()
timeout-minutes: 5
steps:
- name: Check security results
uses: actions/github-script@v7
with:
script: |
const codeql = '${{ needs.codeql.result }}';
const bandit = '${{ needs.python-security.result }}';
console.log('Security Check Results:');
console.log(` CodeQL: ${codeql}`);
console.log(` Bandit: ${bandit}`);
// Only 'failure' is a real failure; 'skipped' is acceptable (e.g., path filters)
const acceptable = ['success', 'skipped'];
const codeqlOk = acceptable.includes(codeql);
const banditOk = acceptable.includes(bandit);
const allPassed = codeqlOk && banditOk;
if (allPassed) {
console.log('\n✅ All security checks passed');
core.summary.addRaw('## ✅ Security Checks Passed\n\nAll security scans completed successfully.');
} else {
console.log('\n❌ Some security checks failed');
core.summary.addRaw('## ❌ Security Checks Failed\n\nOne or more security scans found issues.');
core.setFailed('Security checks failed');
}
await core.summary.write();
@@ -108,7 +108,7 @@ class GraphitiMemory:
if self.group_id_mode == GroupIdMode.PROJECT:
project_name = self.project_dir.name
path_hash = hashlib.md5(
str(self.project_dir.resolve()).encode()
str(self.project_dir.resolve()).encode(), usedforsecurity=False
).hexdigest()[:8]
return f"project_{project_name}_{path_hash}"
else:
@@ -75,7 +75,7 @@ class GraphitiSearch:
if self.group_id_mode == GroupIdMode.SPEC and include_project_context:
project_name = self.project_dir.name
path_hash = hashlib.md5(
str(self.project_dir.resolve()).encode()
str(self.project_dir.resolve()).encode(), usedforsecurity=False
).hexdigest()[:8]
project_group_id = f"project_{project_name}_{path_hash}"
if project_group_id != self.group_id:
+1 -1
View File
@@ -112,7 +112,7 @@ class ProjectAnalyzer:
"docker-compose.yaml",
]
hasher = hashlib.md5()
hasher = hashlib.md5(usedforsecurity=False)
files_found = 0
for filename in hash_files:
+2 -2
View File
@@ -22,7 +22,7 @@ def _compute_file_hash(file_path: Path) -> str:
return ""
try:
content = file_path.read_text(encoding="utf-8")
return hashlib.md5(content.encode("utf-8")).hexdigest()
return hashlib.md5(content.encode("utf-8"), usedforsecurity=False).hexdigest()
except (OSError, UnicodeDecodeError):
return ""
@@ -35,7 +35,7 @@ def _compute_spec_hash(spec_dir: Path) -> str:
spec_hash = _compute_file_hash(spec_dir / "spec.md")
plan_hash = _compute_file_hash(spec_dir / "implementation_plan.json")
combined = f"{spec_hash}:{plan_hash}"
return hashlib.md5(combined.encode("utf-8")).hexdigest()
return hashlib.md5(combined.encode("utf-8"), usedforsecurity=False).hexdigest()
@dataclass
+12 -5
View File
@@ -21,6 +21,7 @@ Usage:
"""
import json
import shlex
import subprocess
import time
from dataclasses import dataclass, field
@@ -178,9 +179,13 @@ class ServiceOrchestrator:
ports = config.get("ports", [])
port = None
if ports:
port_mapping = str(ports[0])
if ":" in port_mapping:
port = int(port_mapping.split(":")[0])
try:
port_mapping = str(ports[0])
if ":" in port_mapping:
port = int(port_mapping.split(":")[0])
except (ValueError, IndexError):
# Skip malformed port mappings (e.g., environment variables)
port = None
# Determine health check URL
health_url = None
@@ -331,9 +336,11 @@ class ServiceOrchestrator:
for service in self._services:
if service.startup_command:
try:
# Use shlex.split() for safe parsing of shell-like syntax
# shell=False prevents shell injection vulnerabilities
proc = subprocess.Popen(
service.startup_command,
shell=True,
shlex.split(service.startup_command),
shell=False,
cwd=self.project_dir / service.path
if service.path
else self.project_dir,
+17
View File
@@ -0,0 +1,17 @@
{
"name": "auto-claude",
"version": "2.7.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "auto-claude",
"version": "2.7.2",
"license": "AGPL-3.0",
"engines": {
"node": ">=24.0.0",
"npm": ">=10.0.0"
}
}
}
}