diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 00000000..2717f1e5 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,13 @@ +name: ConvNetJS Security Scanning +rules: + - query: security-and-quality + - query: security-extended + - query: security-audit + - query: experimental + - query: performance + +exclude: + - path: test/** + - path: demo/** + - path: build/** + - path: compile/** diff --git a/.github/scripts/security-review.js b/.github/scripts/security-review.js new file mode 100644 index 00000000..674f479f --- /dev/null +++ b/.github/scripts/security-review.js @@ -0,0 +1,158 @@ +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +const sarifPath = process.argv[2] || 'results.sarif'; +const reportPath = process.argv[3] || 'security-report.md'; +const openAiKey = process.env.OPENAI_API_KEY; +const openAiModel = process.env.OPENAI_MODEL || 'gpt-4o-mini'; + +function loadSarif(filePath) { + if (!fs.existsSync(filePath)) { + throw new Error(`SARIF file not found: ${filePath}`); + } + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function summarizeFindings(results) { + const severityOrder = ['error', 'warning', 'note', 'none']; + const severityCounts = results.reduce((acc, item) => { + const severity = (item.level || item.kind || 'warning').toLowerCase(); + acc[severity] = (acc[severity] || 0) + 1; + return acc; + }, {}); + + const byRule = results.reduce((acc, item) => { + const ruleId = item.ruleId || 'unknown'; + acc[ruleId] = acc[ruleId] || { count: 0, message: [] }; + acc[ruleId].count += 1; + if (item.message && item.message.text) acc[ruleId].message.push(item.message.text); + return acc; + }, {}); + + return { severityCounts, byRule }; +} + +function formatReport(sarif, summary, topFindings) { + const totalFindings = topFindings.length; + const severityLines = Object.entries(summary.severityCounts) + .sort(([a], [b]) => severityOrder.indexOf(a) - severityOrder.indexOf(b)) + .map(([level, count]) => `- **${level}**: ${count}`) + .join('\n'); + + const findingLines = topFindings.map((item, index) => { + const location = item.locations && item.locations[0] && item.locations[0].physicalLocation; + const file = location && location.artifactLocation && location.artifactLocation.uri; + const region = location && location.region; + const line = region ? region.startLine : 'N/A'; + return [`### Finding ${index + 1}`, + `- **Rule**: ${item.ruleId || 'unknown'}`, + `- **Severity**: ${item.level || item.kind || 'warning'}`, + `- **Message**: ${item.message && item.message.text ? item.message.text : 'N/A'}`, + `- **Location**: ${file || 'unknown'}:${line}`, + ''].join('\n'); + }).join('\n'); + + return [`# Security Report`, + '', + `## Summary`, + '', + `- Total findings: **${totalFindings}**`, + severityLines ? `\n${severityLines}` : '', + '', + `## Top Findings`, + '', + findingLines || 'No findings detected.', + '', + `## Review Notes`, + '', + `The report was generated automatically by the GitHub Actions DevSecOps pipeline.`, + openAiKey ? `An LLM review was requested and its analysis is included below.` : `LLM review was skipped because OPENAI_API_KEY is not configured.`, + '', + `---`, + '', + `*Generated by the DevSecOps pipeline.*`].join('\n'); +} + +function extractTopFindings(results, max = 8) { + return results.slice(0, max); +} + +async function callOpenAI(prompt) { + const body = JSON.stringify({ + model: openAiModel, + messages: [{ role: 'system', content: 'Você é um assistente de segurança de software. Revise os resultados SARIF e gere um resumo conciso com risco, gravidade e recomendações.' }, + { role: 'user', content: prompt }], + max_tokens: 500, + temperature: 0.2 + }); + + const options = { + hostname: 'api.openai.com', + path: '/v1/chat/completions', + method: 'POST', + headers: { + 'Authorization': `Bearer ${openAiKey}`, + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body) + } + }; + + return new Promise((resolve, reject) => { + const req = https.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + const json = JSON.parse(data); + const content = json.choices && json.choices[0] && json.choices[0].message && json.choices[0].message.content; + resolve(content || 'LLM review completed but no content was returned.'); + } catch (error) { + reject(new Error(`Falha ao analisar retorno do OpenAI: ${error.message}`)); + } + } else { + reject(new Error(`OpenAI API error ${res.statusCode}: ${data}`)); + } + }); + }); + + req.on('error', reject); + req.write(body); + req.end(); + }); +} + +async function main() { + const sarif = loadSarif(sarifPath); + const results = (sarif.runs || []).flatMap(run => run.results || []); + const summary = summarizeFindings(results); + const topFindings = extractTopFindings(results); + let report = formatReport(sarif, summary, topFindings); + + if (openAiKey && results.length > 0) { + const prompt = `Aqui estão os ${results.length} resultados SARIF mais importantes. Forneça um resumo de risco, explique os impactos e indique ações corretivas relevantes. Exiba como listas claras e concisas. \n\n` + + topFindings.map((item, index) => { + const location = item.locations && item.locations[0] && item.locations[0].physicalLocation; + const file = location && location.artifactLocation && location.artifactLocation.uri; + const region = location && location.region; + const line = region ? region.startLine : 'N/A'; + return `${index + 1}. Rule: ${item.ruleId || 'unknown'}; Severity: ${item.level || item.kind || 'warning'}; File: ${file || 'unknown'}:${line}; Message: ${item.message && item.message.text ? item.message.text : 'N/A'}`; + }).join('\n'); + + try { + const llmReview = await callOpenAI(prompt); + report += '\n\n## LLM Review\n\n' + llmReview + '\n'; + } catch (error) { + report += `\n\n## LLM Review\n\nFalha ao obter análise LLM: ${error.message}\n`; + } + } + + fs.writeFileSync(reportPath, report, 'utf8'); + console.log(`Security report written to ${reportPath}`); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/.github/workflows/devsecops-security.yml b/.github/workflows/devsecops-security.yml new file mode 100644 index 00000000..37c4f34b --- /dev/null +++ b/.github/workflows/devsecops-security.yml @@ -0,0 +1,95 @@ +name: DevSecOps Security Pipeline + +on: + push: + branches: + - main + - master + - develop + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + schedule: + - cron: '0 3 * * *' + +jobs: + codeql: + name: CodeQL SAST Scan + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + #analysis: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: javascript + config-file: .github/codeql/codeql-config.yml + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v3 + with: + output: results.sarif + + - name: Upload SARIF artifact + uses: actions/upload-artifact@v4 + with: + name: codeql-sarif + path: results.sarif + + security_review: + name: LLM Security Review & PR Comment + runs-on: ubuntu-latest + needs: codeql + if: always() + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download SARIF artifact + uses: actions/download-artifact@v4 + with: + name: codeql-sarif + + - name: Generate security report + run: | + node .github/scripts/security-review.js results.sarif security-report.md + + - name: Upload security report artifact + uses: actions/upload-artifact@v4 + with: + name: security-report + path: security-report.md + + - name: Create automatic security report pull request + if: github.event_name != 'pull_request' + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "docs: update automated security report" + title: "docs: update automated security report" + body: | + This pull request was generated automatically by the DevSecOps Security Pipeline. + + It updates the generated `security-report.md` file from the latest CodeQL SARIF results. + branch: automated/security-report + base: ${{ github.ref_name }} + add-paths: security-report.md + labels: security, automated-pr + + - name: Comment on pull request + if: github.event_name == 'pull_request' + uses: peter-evans/create-or-update-comment@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} + body-file: security-report.md + edit-mode: replace diff --git a/demo/autoencoder.html b/demo/autoencoder.html index e2d37f18..234c8ca1 100644 --- a/demo/autoencoder.html +++ b/demo/autoencoder.html @@ -13,6 +13,7 @@ + diff --git a/demo/cifar10.html b/demo/cifar10.html index a8a1594a..80ac0b5b 100644 --- a/demo/cifar10.html +++ b/demo/cifar10.html @@ -12,6 +12,7 @@ + diff --git a/demo/classify2d.html b/demo/classify2d.html index c933ae6c..cf601aef 100644 --- a/demo/classify2d.html +++ b/demo/classify2d.html @@ -7,6 +7,7 @@ +