- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations). - Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns. - Added `package-lock.json` for dependency management.
200 lines
6.2 KiB
YAML
200 lines
6.2 KiB
YAML
# -----------------------------------------------------------------------------
|
|
# unknowns-budget-gate.yml
|
|
# Sprint: SPRINT_5100_0004_0001_unknowns_budget_ci_gates
|
|
# Task: T2 - CI Budget Gate Workflow
|
|
# Description: Enforces unknowns budgets on PRs and pushes
|
|
# -----------------------------------------------------------------------------
|
|
|
|
name: Unknowns Budget Gate
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- 'src/**'
|
|
- 'Dockerfile*'
|
|
- '*.lock'
|
|
- 'etc/policy.unknowns.yaml'
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- 'src/**'
|
|
- 'Dockerfile*'
|
|
- '*.lock'
|
|
|
|
env:
|
|
DOTNET_NOLOGO: 1
|
|
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
|
TZ: UTC
|
|
STELLAOPS_BUDGET_CONFIG: ./etc/policy.unknowns.yaml
|
|
|
|
jobs:
|
|
scan-and-check-budget:
|
|
runs-on: ubuntu-22.04
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Setup .NET
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: '10.0.100'
|
|
include-prerelease: true
|
|
|
|
- name: Cache NuGet packages
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.nuget/packages
|
|
local-nugets/packages
|
|
key: budget-gate-nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
|
|
|
|
- name: Restore and Build CLI
|
|
run: |
|
|
dotnet restore src/Cli/StellaOps.Cli/StellaOps.Cli.csproj --configfile nuget.config
|
|
dotnet build src/Cli/StellaOps.Cli/StellaOps.Cli.csproj -c Release --no-restore
|
|
|
|
- name: Determine environment
|
|
id: env
|
|
run: |
|
|
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
|
echo "environment=prod" >> $GITHUB_OUTPUT
|
|
echo "enforce=true" >> $GITHUB_OUTPUT
|
|
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
|
echo "environment=stage" >> $GITHUB_OUTPUT
|
|
echo "enforce=false" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "environment=dev" >> $GITHUB_OUTPUT
|
|
echo "enforce=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Create sample verdict for testing
|
|
id: scan
|
|
run: |
|
|
mkdir -p out
|
|
# In a real scenario, this would be from stella scan
|
|
# For now, create a minimal verdict file
|
|
cat > out/verdict.json << 'EOF'
|
|
{
|
|
"unknowns": []
|
|
}
|
|
EOF
|
|
echo "verdict_path=out/verdict.json" >> $GITHUB_OUTPUT
|
|
|
|
- name: Check unknowns budget
|
|
id: budget
|
|
continue-on-error: true
|
|
run: |
|
|
set +e
|
|
dotnet run --project src/Cli/StellaOps.Cli/StellaOps.Cli.csproj -- \
|
|
unknowns budget check \
|
|
--verdict ${{ steps.scan.outputs.verdict_path }} \
|
|
--environment ${{ steps.env.outputs.environment }} \
|
|
--output json \
|
|
--fail-on-exceed > out/budget-result.json
|
|
|
|
EXIT_CODE=$?
|
|
echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT
|
|
|
|
if [ -f out/budget-result.json ]; then
|
|
# Compact JSON for output
|
|
RESULT=$(cat out/budget-result.json | jq -c '.')
|
|
echo "result=$RESULT" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
exit $EXIT_CODE
|
|
|
|
- name: Upload budget report
|
|
uses: actions/upload-artifact@v4
|
|
if: always()
|
|
with:
|
|
name: budget-report-${{ github.run_id }}
|
|
path: out/budget-result.json
|
|
retention-days: 30
|
|
|
|
- name: Post PR comment
|
|
if: github.event_name == 'pull_request' && always()
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const fs = require('fs');
|
|
|
|
let result = { isWithinBudget: true, totalUnknowns: 0 };
|
|
try {
|
|
const content = fs.readFileSync('out/budget-result.json', 'utf8');
|
|
result = JSON.parse(content);
|
|
} catch (e) {
|
|
console.log('Could not read budget result:', e.message);
|
|
}
|
|
|
|
const status = result.isWithinBudget ? ':white_check_mark:' : ':x:';
|
|
const env = '${{ steps.env.outputs.environment }}';
|
|
|
|
let body = `## ${status} Unknowns Budget Check
|
|
|
|
| Metric | Value |
|
|
|--------|-------|
|
|
| Environment | ${env} |
|
|
| Total Unknowns | ${result.totalUnknowns || 0} |
|
|
| Budget Limit | ${result.totalLimit || 'Unlimited'} |
|
|
| Status | ${result.isWithinBudget ? 'PASS' : 'FAIL'} |
|
|
`;
|
|
|
|
if (result.violations && result.violations.length > 0) {
|
|
body += `
|
|
### Violations
|
|
`;
|
|
for (const v of result.violations) {
|
|
body += `- **${v.reasonCode}**: ${v.count}/${v.limit}\n`;
|
|
}
|
|
}
|
|
|
|
if (result.message) {
|
|
body += `\n> ${result.message}\n`;
|
|
}
|
|
|
|
body += `\n---\n_Generated by StellaOps Unknowns Budget Gate_`;
|
|
|
|
// Find existing comment
|
|
const { data: comments } = await github.rest.issues.listComments({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: context.issue.number,
|
|
});
|
|
|
|
const botComment = comments.find(c =>
|
|
c.body.includes('Unknowns Budget Check') &&
|
|
c.user.type === 'Bot'
|
|
);
|
|
|
|
if (botComment) {
|
|
await github.rest.issues.updateComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: botComment.id,
|
|
body: body
|
|
});
|
|
} else {
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: context.issue.number,
|
|
body: body
|
|
});
|
|
}
|
|
|
|
- name: Fail if budget exceeded (prod)
|
|
if: steps.env.outputs.environment == 'prod' && steps.budget.outputs.exit_code == '2'
|
|
run: |
|
|
echo "::error::Production unknowns budget exceeded!"
|
|
exit 1
|
|
|
|
- name: Warn if budget exceeded (non-prod)
|
|
if: steps.env.outputs.environment != 'prod' && steps.budget.outputs.exit_code == '2'
|
|
run: |
|
|
echo "::warning::Unknowns budget exceeded for ${{ steps.env.outputs.environment }}"
|