diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 81a7b2c..ce2b411 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,9 @@ on: permissions: read-all +env: + tag: ${{ github.ref_name }} + jobs: build: runs-on: ubuntu-latest @@ -14,7 +17,6 @@ jobs: contents: write id-token: write env: - tag: ${{ github.ref_name }} os: linux arch: x86_64 outputs: @@ -108,24 +110,22 @@ jobs: provenance: needs: [build] + if: ${{ !endsWith(github.ref_name, 'rc') && !contains(github.ref_name, 'rc.') }} permissions: actions: read id-token: write contents: write uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 - # uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@5a775b367a56d5bd118a224a811bba288150a563 # slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 with: base64-subjects: "${{ needs.build.outputs.hashes }}" - # Upload provenance to a new release upload-assets: true release: - needs: [build, provenance] + needs: [build] runs-on: ubuntu-latest permissions: contents: write env: - tag: ${{ github.ref_name }} os: linux arch: x86_64 steps: @@ -166,7 +166,7 @@ jobs: name: ${{ env.tag }} generate_release_notes: true token: ${{ secrets.GITHUB_TOKEN }} - prerelease: ${{ endsWith(env.tag, 'rc') }} + prerelease: ${{ endsWith(env.tag, 'rc') || contains(env.tag, 'rc.') }} - name: Verify Release run: | diff --git a/.gitignore b/.gitignore index 608b9c7..37d1f31 100644 --- a/.gitignore +++ b/.gitignore @@ -106,4 +106,4 @@ bin/ src/generated/ -audit.json \ No newline at end of file +audit*.json \ No newline at end of file diff --git a/__tests__/index.test.js b/__tests__/index.test.js index 8aea3d3..88ed888 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -44,11 +44,6 @@ describe('index', () => { it('should return an array', async () => { const { getTrustedGithubAccounts } = require('../src/input') - // jest.mock('@actions/core', () => ({ - // warning: (...args) => console.log(...args, '\n'), - // error: (...args) => console.log(...args, '\n') - // })) - const accounts = getTrustedGithubAccounts() expect(accounts).toBeInstanceOf(Array) diff --git a/badges/coverage.svg b/badges/coverage.svg index fd27744..fdd96f7 100644 --- a/badges/coverage.svg +++ b/badges/coverage.svg @@ -1 +1 @@ -Coverage: 16.23%Coverage16.23% \ No newline at end of file +Coverage: 14.44%Coverage14.44% \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index 4624494..13e04e0 100644 --- a/dist/index.js +++ b/dist/index.js @@ -121957,12 +121957,10 @@ async function auditRulesTemplate({ homeDir, workingDir }) { -a exit,always -S execve -k bolt_monitored_process_exec # logs file changes (writes, deletes, renames, etc.) --a exit,always -F dir=%s -F perm=wa -S open,openat,creat,truncate,ftruncate -k file_change +# -a exit,always -F dir=%s -F perm=wa -S open,openat,creat,truncate,ftruncate -k file_change -w ${homeDir} -p wa -k bolt_monitored_bolt_home_changes --w ${workingDir} -p wa -k bolt_monitored_working_dir_changes - -w /etc/passwd -p wa -k bolt_monitored_passwd_changes -w /etc/shadow -p wa -k bolt_monitored_shadow_changes @@ -121976,8 +121974,6 @@ async function auditRulesTemplate({ homeDir, workingDir }) { -w /etc/docker/daemon.json -p wa -k bolt_monitored_docker_daemon_changes -w /var/log/audit/audit.log -p wa -k bolt_monitored_audit_log_changes - --e 2 ` } @@ -121992,6 +121988,7 @@ module.exports = { /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { const core = __nccwpck_require__(42186) +const path = __nccwpck_require__(49411) const { generateTestResults, getUniqueBy } = __nccwpck_require__(70062) @@ -121999,15 +121996,6 @@ const { generateTestResults, getUniqueBy } = __nccwpck_require__(70062) // const boltPID = "1479" // const githubRunnerPID = '1446' -// function printNode(node, depth = 0) { -// if (depth === 0) { -// console.log(node.model.pid) -// } else { -// console.log(`${' '.repeat(depth - 1)}- ${node.model.pid}`) -// } -// node.children.forEach(c => printNode(c, depth + 1)) -// } - function NewNode(pid) { return { pid, @@ -122041,7 +122029,8 @@ function createProcessTree(processTuples) { function parentAction(node) { if (node?.isAction) { - return node + // return node + return parentAction(node.parent) || node } if (node?.parent) { return parentAction(node.parent) @@ -122049,6 +122038,81 @@ function parentAction(node) { return null } +async function getBuildEnvironmentTamperingActions() { + const boltPID = core.getState('boltPID') + const githubRunnerPID = core.getState('githubRunnerPID') + const audit = await generateTestResults('audit.json') + + const buildEnvironmentTamperingEvents = [ + 'bolt_monitored_passwd_changes', + 'bolt_monitored_shadow_changes', + 'bolt_monitored_group_changes', + 'bolt_monitored_sudoers_changes', + 'bolt_monitored_docker_daemon_changes', + 'bolt_monitored_audit_log_changes', + 'bolt_monitored_bolt_home_changes' + ] + + const processTamperingBuildEnv = audit.filter(a => + a.tags?.some(tag => buildEnvironmentTamperingEvents.includes(tag)) + ) +} + +async function checkForBuildTampering() { + const workingDir = process.env.GITHUB_WORKSPACE + const gitDir = path.join(workingDir, '.git') + const absPathGitDir = path.resolve(gitDir) + const audit = await generateTestResults('audit.json') + + const processChangingSourceFiles = audit.filter( + a => + a.tags?.includes('bolt_monitored_wd_changes') && + (a.summary?.action === 'opened-file' || a.summary?.action === 'renamed') + ) + + const filePIDMap = {} + + for (const log of processChangingSourceFiles) { + const pid = log.process?.pid + const cwd = log.process?.cwd + const filePath = log.file?.path + + if (!filePath || !cwd || !pid) { + continue + } + + // Check if the file path is already absolute + const fullFilePath = path.isAbsolute(filePath) + ? filePath + : path.join(cwd, filePath) + + const absPath = path.resolve(fullFilePath) + + if (absPath.startsWith(absPathGitDir)) { + continue + } + + if (pid && fullFilePath) { + if (!filePIDMap[fullFilePath]) { + filePIDMap[fullFilePath] = [] + } + if (!filePIDMap[fullFilePath].includes(pid)) { + filePIDMap[fullFilePath].push(pid) + } + } + } + + const tamperedFiles = [] + + for (const [file, pids] of Object.entries(filePIDMap)) { + if (pids.length > 1) { + tamperedFiles.push(file) + } + } + + return tamperedFiles +} + async function getSudoCallingActions() { const boltPID = core.getState('boltPID') const githubRunnerPID = core.getState('githubRunnerPID') @@ -122096,7 +122160,6 @@ async function getSudoCallingActions() { const nonActionSudoCalls = sudoCalls.filter( a => parentAction(processTree[a.process.pid]) === null ) - // console.log(nonActionSudoCalls.map(a => a.process)); const sudoCallingActions = getUniqueBy( actionSudoCalls.map(a => { @@ -122163,7 +122226,8 @@ async function getAuditSummary() { } module.exports = { - getAuditSummary + getAuditSummary, + checkForBuildTampering } @@ -122258,7 +122322,7 @@ module.exports = { /***/ ((module) => { const auditScriptBase64 = () => { - return 'IyEgL2Jpbi9iYXNoCgpkZWJ1Zz0kMQoKaWYgW1sgIiRkZWJ1ZyIgPT0gInRydWUiIF1dOyB0aGVuCglzZXQgLXgKZmkKCmlmICEgY29tbWFuZCAtdiBhdWRpdGQgJj4vZGV2L251bGw7IHRoZW4KCWVjaG8gIkluc3RhbGxpbmcgYXVkaXRkLi4uIgoJc3VkbyBhcHQtZ2V0IGluc3RhbGwgYXVkaXRkIC15CgllY2hvICJhdWRpdGQgaW5zdGFsbGVkIHN1Y2Nlc3NmdWxseS4iCmVsc2UKCWVjaG8gImF1ZGl0ZCBpcyBhbHJlYWR5IGluc3RhbGxlZC4iCmZpCgojIFNjcmlwdCBleHBlY3RzIGF1ZGl0LnJ1bGVzIGZpbGUgdG8gYmUgaW4gdGhlIHNhbWUgZGlyZWN0b3J5CiMgTW92ZSBhdWRpdC5ydWxlcyBmaWxlIHRvIC9ldGMvYXVkaXQvcnVsZXMuZC8gZGlyZWN0b3J5Cm12IGF1ZGl0LnJ1bGVzIC9ldGMvYXVkaXQvcnVsZXMuZC8KCiMgUmVzdGFydCBhdWRpdGQgc2VydmljZSB0byBhcHBseSB0aGUgbmV3IHJ1bGVzCnNlcnZpY2UgYXVkaXRkIHJlc3RhcnQK' + return 'IyEgL2Jpbi9iYXNoCgp3b3JraW5nRGlyPSQxCmRlYnVnPSQyCgppZiBbWyAiJGRlYnVnIiA9PSAidHJ1ZSIgXV07IHRoZW4KCXNldCAteApmaQoKaWYgISBjb21tYW5kIC12IGF1ZGl0ZCAmPi9kZXYvbnVsbDsgdGhlbgoJZWNobyAiSW5zdGFsbGluZyBhdWRpdGQuLi4iCglzdWRvIGFwdC1nZXQgaW5zdGFsbCBhdWRpdGQgLXkKCWVjaG8gImF1ZGl0ZCBpbnN0YWxsZWQgc3VjY2Vzc2Z1bGx5LiIKZWxzZQoJZWNobyAiYXVkaXRkIGlzIGFscmVhZHkgaW5zdGFsbGVkLiIKZmkKCiMgU2NyaXB0IGV4cGVjdHMgYXVkaXQucnVsZXMgZmlsZSB0byBiZSBpbiB0aGUgc2FtZSBkaXJlY3RvcnkKIyBNb3ZlIGF1ZGl0LnJ1bGVzIGZpbGUgdG8gL2V0Yy9hdWRpdC9ydWxlcy5kLyBkaXJlY3RvcnkKbXYgYXVkaXQucnVsZXMgL2V0Yy9hdWRpdC9ydWxlcy5kLwoKIyBSZXN0YXJ0IGF1ZGl0ZCBzZXJ2aWNlIHRvIGFwcGx5IHRoZSBuZXcgcnVsZXMKc2VydmljZSBhdWRpdGQgcmVzdGFydAoKYXVkaXRjdGwgLXcgIiR3b3JraW5nRGlyIiAtcCB3YSAtayBib2x0X21vbml0b3JlZF93ZF9jaGFuZ2VzCgphdWRpdGN0bCAtZSAyCg==' } module.exports = { @@ -122710,7 +122774,7 @@ async function run() { core.info('Setting up auditd...') const auditRules = await auditRulesTemplate({ homeDir, workingDir }) fs.writeFileSync('audit.rules', auditRules) - await exec(`sudo bash audit.sh ${isDebugMode}`) + await exec(`sudo bash audit.sh ${workingDir} ${isDebugMode}`) core.info('Setting up auditd... done') benchmark('setup-auditd') @@ -122946,7 +123010,7 @@ module.exports = { const core = __nccwpck_require__(42186) const { DefaultArtifactClient } = __nccwpck_require__(79450) const { exec } = __nccwpck_require__(71514) -const { getAuditSummary } = __nccwpck_require__(56022) +const { getAuditSummary, checkForBuildTampering } = __nccwpck_require__(56022) const fs = __nccwpck_require__(57147) const YAML = __nccwpck_require__(44083) const { @@ -122958,6 +123022,7 @@ const { } = __nccwpck_require__(70006) const { generateTestResults, + getGithubCalls, getUniqueBy, getRawCollapsible } = __nccwpck_require__(70062) @@ -122967,6 +123032,8 @@ const allowHTTP = getAllowHTTP() const defaultPolicy = getDefaultPolicy() const egressRules = getEgressRules() const trustedGithubAccounts = getTrustedGithubAccounts() +const repoName = process.env.GITHUB_REPOSITORY // e.g. koalalab-inc/bolt +const repoOwner = repoName.split('/')[0] // e.g. koalalab-inc function actionString(action) { switch (action) { @@ -123008,6 +123075,8 @@ async function generateSummary() { const randomString = Math.random().toString(36).substring(7) const jobName = `${jobID}-${runId}-${runAttempt}-${runNumber}-${randomString}` + const githubCallsFilename = `${homeDir}/github_calls.json` + if (isDebugMode === 'true') { // Upload auditd log file to artifacts const artifactName = `${jobName}-bolt-auditd-log` @@ -123043,19 +123112,41 @@ async function generateSummary() { await exec(`cp ${homeDir}/${outputFile} ${outputFile}`) const results = await generateTestResults(outputFile) + const githubCalls = getGithubCalls(githubCallsFilename) const uniqueResults = getUniqueBy(results, ['destination', 'scheme']) // const uniqueResultRows = uniqueResults.map(resultToRow) - const githubAccountCalls = results.filter(result => { - return result.trusted_github_account_flag !== undefined - }) + // const githubAccountCalls = results.filter(result => { + // return result.trusted_github_account_flag !== undefined + // }) - const githubAccounts = githubAccountCalls.reduce((accounts, call) => { - const path = call.request_path - const method = call.request_method - const name = call.github_account_name - const trusted_flag = call.trusted_github_account_flag + // const githubAccounts = githubAccountCalls.reduce((accounts, call) => { + // const path = call.request_path + // const method = call.request_method + // const name = call.github_account_name + // const trusted_flag = call.trusted_github_account_flag + // accounts[name] = accounts[name] || {} + // accounts[name]['name'] = name + // accounts[name]['trusted'] = trusted_flag + // const paths = accounts[name]['paths'] || [] + // if (!paths.some(p => p.path === path)) { + // accounts[name]['paths'] = [...paths, { path, method }] + // } + // return accounts + // }, []) + + const githubAccounts = githubCalls.reduce((accounts, call) => { + const path = call.path + const method = call.method + let name = '' + if (path.startsWith('/orgs/') || path.startsWith('/repos/')) { + const parts = path.split('/') + name = parts[2] + } + + const trusted_flag = + trustedGithubAccounts.includes(name) || name === repoOwner accounts[name] = accounts[name] || {} accounts[name]['name'] = name accounts[name]['trusted'] = trusted_flag @@ -123135,15 +123226,15 @@ async function generateSummary() { const configTableString = core.summary.addTable(configTable).stringify() core.summary.emptyBuffer() - // const trustedGithubAccountsHeaderString = core.summary - // .addHeading('🔒 Trusted Github Accounts', 4) - // .stringify() - // core.summary.emptyBuffer() + const trustedGithubAccountsHeaderString = core.summary + .addHeading('🔒 Trusted Github Accounts', 4) + .stringify() + core.summary.emptyBuffer() - // const trustedGithubAccountsTableString = core.summary - // .addTable(trustedGithubAccountsData) - // .stringify() - // core.summary.emptyBuffer() + const trustedGithubAccountsTableString = core.summary + .addTable(trustedGithubAccountsData) + .stringify() + core.summary.emptyBuffer() const knownDestinationsHeaderString = core.summary .addHeading('✅ Known Destinations', 4) @@ -123167,6 +123258,13 @@ async function generateSummary() { const auditSummary = await getAuditSummary() + const tamperedFiles = await checkForBuildTampering() + + const tamperedFilesData = [ + [{ data: 'Tampered Files', header: true }], + ...tamperedFiles.map(file => [file]) + ] + const auditSummaryRaw = auditSummary.zeroState ? auditSummary.zeroState : getRawCollapsible(auditSummary) @@ -123186,20 +123284,20 @@ ${configTableString} ` ) - // if (trustedGithubAccounts.length > 0) { - // summary = summary - // .addRaw( - // ` - //
- // - // ${trustedGithubAccountsHeaderString} - // - // ${trustedGithubAccountsTableString} - //
- // ` - // ) - // .addQuote('NOTE: The account in which workflow runs is always trusted.') - // } + if (trustedGithubAccounts.length > 0) { + summary = summary + .addRaw( + ` +
+ + ${trustedGithubAccountsHeaderString} + + ${trustedGithubAccountsTableString} +
+ ` + ) + .addQuote('NOTE: The account in which workflow runs is always trusted.') + } if (egressRules.length > 0) { summary = summary @@ -123216,28 +123314,37 @@ ${configTableString} .addEOL() } - // if (untrustedGithubAccounts.length > 0) { - // summary = summary.addHeading( - // '🚨 Requests to untrusted GitHub accounts found', - // 3 - // ).addRaw(` - // > [!CAUTION] - // > If you do not recognize these GitHub Accounts, you may want to investigate further. Add them to your trusted GitHub accounts if this is expected. See [Docs](https://github.com/koalalab-inc/bolt?tab=readme-ov-file#configure) for more information. - // `) - - // for (const account of untrustedGithubAccounts) { - // summary = summary.addRaw(` - //
- // - // ${account.name} - // - // - //
- // `) - // } - // } + if (untrustedGithubAccounts.length > 0) { + summary = summary.addHeading( + '🚨 Requests to untrusted GitHub accounts found', + 3 + ).addRaw(` + > [!CAUTION] + > If you do not recognize these GitHub Accounts, you may want to investigate further. Add them to your trusted GitHub accounts if this is expected. See [Docs](https://github.com/koalalab-inc/bolt?tab=readme-ov-file#configure) for more information. + `) + + for (const account of untrustedGithubAccounts) { + summary = summary.addRaw(` +
+ + ${account.name} + + +
+ `) + } + } + + if (tamperedFiles.length > 0) { + summary = summary.addHeading('🚨 File tampering detected', 3).addRaw(` + > [!CAUTION] + > Source files were edited after being fetched from the repository. This may be a security risk. Investigate further. + `) + + summary = summary.addTable(tamperedFilesData) + } summary = summary.addRaw(auditSummaryRaw) @@ -123265,7 +123372,7 @@ ${unknownDestinationsTableString} ) .addRaw( ` -
+
${knownDestinationsHeaderString} @@ -123319,6 +123426,30 @@ async function generateTestResults(filePath) { } } +function getGithubCalls(githubCallsFilename) { + try { + const githubCallsFileContent = fs.readFileSync(githubCallsFilename, 'utf-8') + const lines = githubCallsFileContent.split('\n') + + const githubCalls = [] + + for (const line of lines) { + if (line.length === 0) continue + try { + const githubCall = JSON.parse(line) + githubCalls.push(githubCall) + } catch (error) { + console.error(`Error parsing JSON on line: ${line}`) + } + } + + return githubCalls + } catch (error) { + console.error(`Error reading file: ${error.message}`) + return [] + } +} + function getUniqueBy(arr, keys) { const uniqueObj = arr.reduce((unique, o) => { const key = keys.map(k => o[k]).join('|') @@ -123341,6 +123472,7 @@ function getRawCollapsible({ body, header }) { module.exports = { generateTestResults, + getGithubCalls, getUniqueBy, getRawCollapsible } @@ -123351,7 +123483,7 @@ module.exports = { /***/ 49554: /***/ ((module) => { -const releaseVersion = 'v1.6.2' +const releaseVersion = 'v1.7.0' module.exports = { releaseVersion @@ -123520,6 +123652,14 @@ module.exports = require("node:events"); /***/ }), +/***/ 49411: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:path"); + +/***/ }), + /***/ 84492: /***/ ((module) => { diff --git a/package-lock.json b/package-lock.json index c54c575..d16e181 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bolt", - "version": "1.4.2-rc", + "version": "v1.7.0-rc", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bolt", - "version": "1.4.2-rc", + "version": "v1.7.0-rc", "license": "MIT", "dependencies": { "@actions/artifact": "^2.1.9", diff --git a/package.json b/package.json index 1166185..8eee335 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bolt", "description": "A GitHub Action to install and run Bolt", - "version": "1.4.2-rc", + "version": "v1.7.0-rc", "author": "Abhishek Anand", "private": true, "homepage": "https://github.com/koalalab-inc/bolt#readme", diff --git a/src/audit_rules.js b/src/audit_rules.js index 6db0eeb..8a6f919 100644 --- a/src/audit_rules.js +++ b/src/audit_rules.js @@ -10,12 +10,10 @@ async function auditRulesTemplate({ homeDir, workingDir }) { -a exit,always -S execve -k bolt_monitored_process_exec # logs file changes (writes, deletes, renames, etc.) --a exit,always -F dir=%s -F perm=wa -S open,openat,creat,truncate,ftruncate -k file_change +# -a exit,always -F dir=%s -F perm=wa -S open,openat,creat,truncate,ftruncate -k file_change -w ${homeDir} -p wa -k bolt_monitored_bolt_home_changes --w ${workingDir} -p wa -k bolt_monitored_working_dir_changes - -w /etc/passwd -p wa -k bolt_monitored_passwd_changes -w /etc/shadow -p wa -k bolt_monitored_shadow_changes @@ -29,8 +27,6 @@ async function auditRulesTemplate({ homeDir, workingDir }) { -w /etc/docker/daemon.json -p wa -k bolt_monitored_docker_daemon_changes -w /var/log/audit/audit.log -p wa -k bolt_monitored_audit_log_changes - --e 2 ` } diff --git a/src/audit_summary.js b/src/audit_summary.js index 3f4f579..7da0425 100644 --- a/src/audit_summary.js +++ b/src/audit_summary.js @@ -1,4 +1,5 @@ const core = require('@actions/core') +const path = require('node:path') const { generateTestResults, getUniqueBy } = require('./summary_utils') @@ -6,15 +7,6 @@ const { generateTestResults, getUniqueBy } = require('./summary_utils') // const boltPID = "1479" // const githubRunnerPID = '1446' -// function printNode(node, depth = 0) { -// if (depth === 0) { -// console.log(node.model.pid) -// } else { -// console.log(`${' '.repeat(depth - 1)}- ${node.model.pid}`) -// } -// node.children.forEach(c => printNode(c, depth + 1)) -// } - function NewNode(pid) { return { pid, @@ -48,7 +40,8 @@ function createProcessTree(processTuples) { function parentAction(node) { if (node?.isAction) { - return node + // return node + return parentAction(node.parent) || node } if (node?.parent) { return parentAction(node.parent) @@ -56,6 +49,81 @@ function parentAction(node) { return null } +async function getBuildEnvironmentTamperingActions() { + const boltPID = core.getState('boltPID') + const githubRunnerPID = core.getState('githubRunnerPID') + const audit = await generateTestResults('audit.json') + + const buildEnvironmentTamperingEvents = [ + 'bolt_monitored_passwd_changes', + 'bolt_monitored_shadow_changes', + 'bolt_monitored_group_changes', + 'bolt_monitored_sudoers_changes', + 'bolt_monitored_docker_daemon_changes', + 'bolt_monitored_audit_log_changes', + 'bolt_monitored_bolt_home_changes' + ] + + const processTamperingBuildEnv = audit.filter(a => + a.tags?.some(tag => buildEnvironmentTamperingEvents.includes(tag)) + ) +} + +async function checkForBuildTampering() { + const workingDir = process.env.GITHUB_WORKSPACE + const gitDir = path.join(workingDir, '.git') + const absPathGitDir = path.resolve(gitDir) + const audit = await generateTestResults('audit.json') + + const processChangingSourceFiles = audit.filter( + a => + a.tags?.includes('bolt_monitored_wd_changes') && + (a.summary?.action === 'opened-file' || a.summary?.action === 'renamed') + ) + + const filePIDMap = {} + + for (const log of processChangingSourceFiles) { + const pid = log.process?.pid + const cwd = log.process?.cwd + const filePath = log.file?.path + + if (!filePath || !cwd || !pid) { + continue + } + + // Check if the file path is already absolute + const fullFilePath = path.isAbsolute(filePath) + ? filePath + : path.join(cwd, filePath) + + const absPath = path.resolve(fullFilePath) + + if (absPath.startsWith(absPathGitDir)) { + continue + } + + if (pid && fullFilePath) { + if (!filePIDMap[fullFilePath]) { + filePIDMap[fullFilePath] = [] + } + if (!filePIDMap[fullFilePath].includes(pid)) { + filePIDMap[fullFilePath].push(pid) + } + } + } + + const tamperedFiles = [] + + for (const [file, pids] of Object.entries(filePIDMap)) { + if (pids.length > 1) { + tamperedFiles.push(file) + } + } + + return tamperedFiles +} + async function getSudoCallingActions() { const boltPID = core.getState('boltPID') const githubRunnerPID = core.getState('githubRunnerPID') @@ -103,7 +171,6 @@ async function getSudoCallingActions() { const nonActionSudoCalls = sudoCalls.filter( a => parentAction(processTree[a.process.pid]) === null ) - // console.log(nonActionSudoCalls.map(a => a.process)); const sudoCallingActions = getUniqueBy( actionSudoCalls.map(a => { @@ -170,5 +237,6 @@ async function getAuditSummary() { } module.exports = { - getAuditSummary + getAuditSummary, + checkForBuildTampering } diff --git a/src/main.js b/src/main.js index f98e602..322aa1e 100644 --- a/src/main.js +++ b/src/main.js @@ -92,7 +92,7 @@ async function run() { core.info('Setting up auditd...') const auditRules = await auditRulesTemplate({ homeDir, workingDir }) fs.writeFileSync('audit.rules', auditRules) - await exec(`sudo bash audit.sh ${isDebugMode}`) + await exec(`sudo bash audit.sh ${workingDir} ${isDebugMode}`) core.info('Setting up auditd... done') benchmark('setup-auditd') diff --git a/src/scripts/audit.sh b/src/scripts/audit.sh index a4c5719..7514fd3 100755 --- a/src/scripts/audit.sh +++ b/src/scripts/audit.sh @@ -1,6 +1,7 @@ #! /bin/bash -debug=$1 +workingDir=$1 +debug=$2 if [[ "$debug" == "true" ]]; then set -x @@ -20,3 +21,7 @@ mv audit.rules /etc/audit/rules.d/ # Restart auditd service to apply the new rules service auditd restart + +auditctl -w "$workingDir" -p wa -k bolt_monitored_wd_changes + +auditctl -e 2 diff --git a/src/summary.js b/src/summary.js index 684f56e..9762d95 100644 --- a/src/summary.js +++ b/src/summary.js @@ -1,7 +1,7 @@ const core = require('@actions/core') const { DefaultArtifactClient } = require('@actions/artifact') const { exec } = require('@actions/exec') -const { getAuditSummary } = require('./audit_summary') +const { getAuditSummary, checkForBuildTampering } = require('./audit_summary') const fs = require('fs') const YAML = require('yaml') const { @@ -13,6 +13,7 @@ const { } = require('./input') const { generateTestResults, + getGithubCalls, getUniqueBy, getRawCollapsible } = require('./summary_utils') @@ -22,6 +23,8 @@ const allowHTTP = getAllowHTTP() const defaultPolicy = getDefaultPolicy() const egressRules = getEgressRules() const trustedGithubAccounts = getTrustedGithubAccounts() +const repoName = process.env.GITHUB_REPOSITORY // e.g. koalalab-inc/bolt +const repoOwner = repoName.split('/')[0] // e.g. koalalab-inc function actionString(action) { switch (action) { @@ -63,6 +66,8 @@ async function generateSummary() { const randomString = Math.random().toString(36).substring(7) const jobName = `${jobID}-${runId}-${runAttempt}-${runNumber}-${randomString}` + const githubCallsFilename = `${homeDir}/github_calls.json` + if (isDebugMode === 'true') { // Upload auditd log file to artifacts const artifactName = `${jobName}-bolt-auditd-log` @@ -98,19 +103,41 @@ async function generateSummary() { await exec(`cp ${homeDir}/${outputFile} ${outputFile}`) const results = await generateTestResults(outputFile) + const githubCalls = getGithubCalls(githubCallsFilename) const uniqueResults = getUniqueBy(results, ['destination', 'scheme']) // const uniqueResultRows = uniqueResults.map(resultToRow) - const githubAccountCalls = results.filter(result => { - return result.trusted_github_account_flag !== undefined - }) + // const githubAccountCalls = results.filter(result => { + // return result.trusted_github_account_flag !== undefined + // }) + + // const githubAccounts = githubAccountCalls.reduce((accounts, call) => { + // const path = call.request_path + // const method = call.request_method + // const name = call.github_account_name + // const trusted_flag = call.trusted_github_account_flag + // accounts[name] = accounts[name] || {} + // accounts[name]['name'] = name + // accounts[name]['trusted'] = trusted_flag + // const paths = accounts[name]['paths'] || [] + // if (!paths.some(p => p.path === path)) { + // accounts[name]['paths'] = [...paths, { path, method }] + // } + // return accounts + // }, []) + + const githubAccounts = githubCalls.reduce((accounts, call) => { + const path = call.path + const method = call.method + let name = '' + if (path.startsWith('/orgs/') || path.startsWith('/repos/')) { + const parts = path.split('/') + name = parts[2] + } - const githubAccounts = githubAccountCalls.reduce((accounts, call) => { - const path = call.request_path - const method = call.request_method - const name = call.github_account_name - const trusted_flag = call.trusted_github_account_flag + const trusted_flag = + trustedGithubAccounts.includes(name) || name === repoOwner accounts[name] = accounts[name] || {} accounts[name]['name'] = name accounts[name]['trusted'] = trusted_flag @@ -190,15 +217,15 @@ async function generateSummary() { const configTableString = core.summary.addTable(configTable).stringify() core.summary.emptyBuffer() - // const trustedGithubAccountsHeaderString = core.summary - // .addHeading('🔒 Trusted Github Accounts', 4) - // .stringify() - // core.summary.emptyBuffer() + const trustedGithubAccountsHeaderString = core.summary + .addHeading('🔒 Trusted Github Accounts', 4) + .stringify() + core.summary.emptyBuffer() - // const trustedGithubAccountsTableString = core.summary - // .addTable(trustedGithubAccountsData) - // .stringify() - // core.summary.emptyBuffer() + const trustedGithubAccountsTableString = core.summary + .addTable(trustedGithubAccountsData) + .stringify() + core.summary.emptyBuffer() const knownDestinationsHeaderString = core.summary .addHeading('✅ Known Destinations', 4) @@ -222,6 +249,13 @@ async function generateSummary() { const auditSummary = await getAuditSummary() + const tamperedFiles = await checkForBuildTampering() + + const tamperedFilesData = [ + [{ data: 'Tampered Files', header: true }], + ...tamperedFiles.map(file => [file]) + ] + const auditSummaryRaw = auditSummary.zeroState ? auditSummary.zeroState : getRawCollapsible(auditSummary) @@ -241,20 +275,20 @@ ${configTableString} ` ) - // if (trustedGithubAccounts.length > 0) { - // summary = summary - // .addRaw( - // ` - //
- // - // ${trustedGithubAccountsHeaderString} - // - // ${trustedGithubAccountsTableString} - //
- // ` - // ) - // .addQuote('NOTE: The account in which workflow runs is always trusted.') - // } + if (trustedGithubAccounts.length > 0) { + summary = summary + .addRaw( + ` +
+ + ${trustedGithubAccountsHeaderString} + + ${trustedGithubAccountsTableString} +
+ ` + ) + .addQuote('NOTE: The account in which workflow runs is always trusted.') + } if (egressRules.length > 0) { summary = summary @@ -271,28 +305,37 @@ ${configTableString} .addEOL() } - // if (untrustedGithubAccounts.length > 0) { - // summary = summary.addHeading( - // '🚨 Requests to untrusted GitHub accounts found', - // 3 - // ).addRaw(` - // > [!CAUTION] - // > If you do not recognize these GitHub Accounts, you may want to investigate further. Add them to your trusted GitHub accounts if this is expected. See [Docs](https://github.com/koalalab-inc/bolt?tab=readme-ov-file#configure) for more information. - // `) - - // for (const account of untrustedGithubAccounts) { - // summary = summary.addRaw(` - //
- // - // ${account.name} - // - //
    - // ${account.paths.map(({ method, path }) => `
  • [${method}] ${path}
  • `).join('')} - //
- //
- // `) - // } - // } + if (untrustedGithubAccounts.length > 0) { + summary = summary.addHeading( + '🚨 Requests to untrusted GitHub accounts found', + 3 + ).addRaw(` + > [!CAUTION] + > If you do not recognize these GitHub Accounts, you may want to investigate further. Add them to your trusted GitHub accounts if this is expected. See [Docs](https://github.com/koalalab-inc/bolt?tab=readme-ov-file#configure) for more information. + `) + + for (const account of untrustedGithubAccounts) { + summary = summary.addRaw(` +
+ + ${account.name} + +
    + ${account.paths.map(({ method, path }) => `
  • [${method}] ${path}
  • `).join('')} +
+
+ `) + } + } + + if (tamperedFiles.length > 0) { + summary = summary.addHeading('🚨 File tampering detected', 3).addRaw(` + > [!CAUTION] + > Source files were edited after being fetched from the repository. This may be a security risk. Investigate further. + `) + + summary = summary.addTable(tamperedFilesData) + } summary = summary.addRaw(auditSummaryRaw) @@ -320,7 +363,7 @@ ${unknownDestinationsTableString} ) .addRaw( ` -
+
${knownDestinationsHeaderString} diff --git a/src/summary_utils.js b/src/summary_utils.js index 4e5cd72..e8a67e6 100644 --- a/src/summary_utils.js +++ b/src/summary_utils.js @@ -27,6 +27,30 @@ async function generateTestResults(filePath) { } } +function getGithubCalls(githubCallsFilename) { + try { + const githubCallsFileContent = fs.readFileSync(githubCallsFilename, 'utf-8') + const lines = githubCallsFileContent.split('\n') + + const githubCalls = [] + + for (const line of lines) { + if (line.length === 0) continue + try { + const githubCall = JSON.parse(line) + githubCalls.push(githubCall) + } catch (error) { + console.error(`Error parsing JSON on line: ${line}`) + } + } + + return githubCalls + } catch (error) { + console.error(`Error reading file: ${error.message}`) + return [] + } +} + function getUniqueBy(arr, keys) { const uniqueObj = arr.reduce((unique, o) => { const key = keys.map(k => o[k]).join('|') @@ -49,6 +73,7 @@ function getRawCollapsible({ body, header }) { module.exports = { generateTestResults, + getGithubCalls, getUniqueBy, getRawCollapsible } diff --git a/src/version.js b/src/version.js index 30b8ea9..03c1fd5 100644 --- a/src/version.js +++ b/src/version.js @@ -1,4 +1,4 @@ -const releaseVersion = 'v1.6.2' +const releaseVersion = 'v1.7.0' module.exports = { releaseVersion