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% Coverage Coverage 16.23% 16.23%
\ No newline at end of file
+Coverage: 14.44% Coverage Coverage 14.44% 14.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}
- //
- //
- // ${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)
@@ -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