diff --git a/ql/lib/codeql/actions/Ast.qll b/ql/lib/codeql/actions/Ast.qll index 17b0dab4ee6..63f2552f582 100644 --- a/ql/lib/codeql/actions/Ast.qll +++ b/ql/lib/codeql/actions/Ast.qll @@ -13,6 +13,8 @@ class AstNode instanceof AstNodeImpl { string toString() { result = super.toString() } + Step getEnclosingStep() { result = super.getEnclosingStep() } + Job getEnclosingJob() { result = super.getEnclosingJob() } Workflow getEnclosingWorkflow() { result = super.getEnclosingWorkflow() } diff --git a/ql/lib/codeql/actions/ast/internal/Ast.qll b/ql/lib/codeql/actions/ast/internal/Ast.qll index 5361943331b..d4716f89e19 100644 --- a/ql/lib/codeql/actions/ast/internal/Ast.qll +++ b/ql/lib/codeql/actions/ast/internal/Ast.qll @@ -110,6 +110,18 @@ abstract class AstNodeImpl extends TAstNode { result = this.getEnclosingCompositeAction().getACallerJob() } + /** + * Gets the enclosing Step. + */ + StepImpl getEnclosingStep() { + if this instanceof StepImpl + then result = this + else + if this instanceof ScalarValueImpl + then result.getAChildNode*() = this.getParentNode() + else none() + } + /** * Gets the enclosing workflow if any. */ diff --git a/ql/lib/codeql/actions/security/ControlChecks.qll b/ql/lib/codeql/actions/security/ControlChecks.qll index 052b22cd338..b9410f0fcb0 100644 --- a/ql/lib/codeql/actions/security/ControlChecks.qll +++ b/ql/lib/codeql/actions/security/ControlChecks.qll @@ -37,29 +37,29 @@ abstract class ControlCheck extends AstNode { this instanceof Run } - predicate protects(Step step, Event event, string category) { + predicate protects(AstNode node, Event event, string category) { // The check dominates the step it should protect - this.dominates(step) and + this.dominates(node) and // The check is effective against the event and category this.protectsCategoryAndEvent(category, event.getName()) and // The check can be triggered by the event this.getEnclosingJob().getATriggerEvent() = event } - predicate dominates(Step step) { + predicate dominates(AstNode node) { this instanceof If and ( - step.getIf() = this or - step.getEnclosingJob().getIf() = this or - step.getEnclosingJob().getANeededJob().(LocalJob).getAStep().getIf() = this or - step.getEnclosingJob().getANeededJob().(LocalJob).getIf() = this + node.getEnclosingStep().getIf() = this or + node.getEnclosingJob().getIf() = this or + node.getEnclosingJob().getANeededJob().(LocalJob).getAStep().getIf() = this or + node.getEnclosingJob().getANeededJob().(LocalJob).getIf() = this ) or this instanceof Environment and ( - step.getEnclosingJob().getEnvironment() = this + node.getEnclosingJob().getEnvironment() = this or - step.getEnclosingJob().getANeededJob().getEnvironment() = this + node.getEnclosingJob().getANeededJob().getEnvironment() = this ) or ( @@ -67,9 +67,9 @@ abstract class ControlCheck extends AstNode { this instanceof UsesStep ) and ( - this.(Step).getAFollowingStep() = step + this.(Step).getAFollowingStep() = node.getEnclosingStep() or - step.getEnclosingJob().getANeededJob().(LocalJob).getAStep() = this.(Step) + node.getEnclosingJob().getANeededJob().(LocalJob).getAStep() = this.(Step) ) } diff --git a/ql/src/Security/CWE-077/EnvVarInjectionCritical.ql b/ql/src/Security/CWE-077/EnvVarInjectionCritical.ql index b301915d79c..ad97dd3caef 100644 --- a/ql/src/Security/CWE-077/EnvVarInjectionCritical.ql +++ b/ql/src/Security/CWE-077/EnvVarInjectionCritical.ql @@ -22,19 +22,20 @@ from EnvVarInjectionFlow::PathNode source, EnvVarInjectionFlow::PathNode sink, E where EnvVarInjectionFlow::flowPath(source, sink) and inPrivilegedContext(sink.getNode().asExpr(), event) and - not exists(ControlCheck check | - check.protects(sink.getNode().asExpr(), event, "envvar-injection") - ) and // exclude paths to file read sinks from non-artifact sources ( + // source is text not source.getNode().(RemoteFlowSource).getSourceType() = "artifact" and not exists(ControlCheck check | - check.protects(sink.getNode().asExpr(), event, "code-injection") + check.protects(sink.getNode().asExpr(), event, ["envvar-injection", "code-injection"]) ) or + // source is an artifact or a file from an untrusted checkout source.getNode().(RemoteFlowSource).getSourceType() = "artifact" and not exists(ControlCheck check | - check.protects(sink.getNode().asExpr(), event, ["untrusted-checkout", "artifact-poisoning"]) + check + .protects(sink.getNode().asExpr(), event, + ["envvar-injection", "untrusted-checkout", "artifact-poisoning"]) ) and ( sink.getNode() instanceof EnvVarInjectionFromFileReadSink or diff --git a/ql/src/Security/CWE-078/CommandInjectionCritical.ql b/ql/src/Security/CWE-078/CommandInjectionCritical.ql index 80281e8db30..c3d6fa74f6c 100644 --- a/ql/src/Security/CWE-078/CommandInjectionCritical.ql +++ b/ql/src/Security/CWE-078/CommandInjectionCritical.ql @@ -16,11 +16,15 @@ import actions import codeql.actions.security.CommandInjectionQuery import CommandInjectionFlow::PathGraph +import codeql.actions.security.ControlChecks from CommandInjectionFlow::PathNode source, CommandInjectionFlow::PathNode sink, Event event where CommandInjectionFlow::flowPath(source, sink) and - inPrivilegedContext(sink.getNode().asExpr(), event) + inPrivilegedContext(sink.getNode().asExpr(), event) and + not exists(ControlCheck check | + check.protects(sink.getNode().asExpr(), event, ["command-injection", "code-injection"]) + ) select sink.getNode(), source, sink, "Potential command injection in $@, which may be controlled by an external user.", sink, sink.getNode().asExpr().(Expression).getRawExpression() diff --git a/ql/test/query-tests/Security/CWE-077/.github/workflows/test11.yml b/ql/test/query-tests/Security/CWE-077/.github/workflows/test11.yml new file mode 100644 index 00000000000..2c2480f5353 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-077/.github/workflows/test11.yml @@ -0,0 +1,81 @@ +name: Write prerelease comment + +on: + workflow_run: + workflows: ["Create Pull Request Prerelease"] + types: + - completed + +jobs: + comment: + if: ${{ github.repository_owner == 'cloudflare' }} + runs-on: ubuntu-latest + name: Write comment to the PR + steps: + - name: "Put PR and workflow ID on the environment" + uses: actions/github-script@v7 + with: + script: | + // Copied from .github/extract-pr-and-workflow-id.js + const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + + for (const artifact of allArtifacts.data.artifacts) { + // Extract the PR number from the artifact name + const match = /^npm-package-(.+)-(\d+)$/.exec(artifact.name); + if (match) { + const packageName = match[1].toUpperCase(); + require("fs").appendFileSync( + process.env.GITHUB_ENV, + `\nWORKFLOW_RUN_PR_FOR_${packageName}=${match[2]}` + + `\nWORKFLOW_RUN_ID_FOR_${packageName}=${context.payload.workflow_run.id}` + ); + } + } + + - name: "Download runtime versions" + # Regular `actions/download-artifact` doesn't support downloading + # artifacts from another workflow + uses: dawidd6/action-download-artifact@v2 + with: + run_id: ${{ github.event.workflow_run.id }} + name: runtime-versions.md + + - name: "Put runtime versions on the environment" + id: runtime_versions + run: | + { + echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV" + + - name: "Download pre-release report" + uses: dawidd6/action-download-artifact@v2 + with: + run_id: ${{ github.event.workflow_run.id }} + name: prerelease-report.md + + - name: "Put pre-release report on the environment" + id: prerelease_report + run: | + { + echo 'PRERELEASE_REPORT<> "$GITHUB_ENV" + + - name: "Comment on PR with Wrangler link" + uses: marocchino/sticky-pull-request-comment@v2 + with: + number: ${{ env.WORKFLOW_RUN_PR_FOR_WRANGLER }} + message: | + ${{ env.PRERELEASE_REPORT }} + + --- + + ${{ env.RUNTIME_VERSIONS }} + diff --git a/ql/test/query-tests/Security/CWE-077/.github/workflows/test12.yml b/ql/test/query-tests/Security/CWE-077/.github/workflows/test12.yml new file mode 100644 index 00000000000..3a0c4cc91b8 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-077/.github/workflows/test12.yml @@ -0,0 +1,80 @@ +name: Write prerelease comment + +on: + workflow_run: + workflows: ["Create Pull Request Prerelease"] + types: + - completed + +jobs: + comment: + runs-on: ubuntu-latest + name: Write comment to the PR + steps: + - name: "Put PR and workflow ID on the environment" + uses: actions/github-script@v7 + with: + script: | + // Copied from .github/extract-pr-and-workflow-id.js + const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + + for (const artifact of allArtifacts.data.artifacts) { + // Extract the PR number from the artifact name + const match = /^npm-package-(.+)-(\d+)$/.exec(artifact.name); + if (match) { + const packageName = match[1].toUpperCase(); + require("fs").appendFileSync( + process.env.GITHUB_ENV, + `\nWORKFLOW_RUN_PR_FOR_${packageName}=${match[2]}` + + `\nWORKFLOW_RUN_ID_FOR_${packageName}=${context.payload.workflow_run.id}` + ); + } + } + + - name: "Download runtime versions" + # Regular `actions/download-artifact` doesn't support downloading + # artifacts from another workflow + uses: dawidd6/action-download-artifact@v2 + with: + run_id: ${{ github.event.workflow_run.id }} + name: runtime-versions.md + + - name: "Put runtime versions on the environment" + id: runtime_versions + run: | + { + echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV" + + - name: "Download pre-release report" + uses: dawidd6/action-download-artifact@v2 + with: + run_id: ${{ github.event.workflow_run.id }} + name: prerelease-report.md + + - name: "Put pre-release report on the environment" + id: prerelease_report + run: | + { + echo 'PRERELEASE_REPORT<> "$GITHUB_ENV" + + - name: "Comment on PR with Wrangler link" + uses: marocchino/sticky-pull-request-comment@v2 + with: + number: ${{ env.WORKFLOW_RUN_PR_FOR_WRANGLER }} + message: | + ${{ env.PRERELEASE_REPORT }} + + --- + + ${{ env.RUNTIME_VERSIONS }} + diff --git a/ql/test/query-tests/Security/CWE-077/EnvVarInjectionCritical.expected b/ql/test/query-tests/Security/CWE-077/EnvVarInjectionCritical.expected index 359275aef43..cbd17161942 100644 --- a/ql/test/query-tests/Security/CWE-077/EnvVarInjectionCritical.expected +++ b/ql/test/query-tests/Security/CWE-077/EnvVarInjectionCritical.expected @@ -20,6 +20,14 @@ edges | .github/workflows/test8.yml:26:9:32:6 | Uses Step | .github/workflows/test8.yml:40:14:41:79 | echo "foo=$(< /artifacts/parent-artifacts/event.txt)" >> $GITHUB_ENV\n | provenance | | | .github/workflows/test9.yml:19:9:27:6 | Uses Step | .github/workflows/test9.yml:29:14:41:41 | pr_num=$(jq -r '.pull_request.number' artifacts/event_file/event.json)\nif [ -z "$pr_num" ] \|\| [ "$pr_num" == "null" ]; then\n pr_num=""\nfi\n\nref=$pr_num\nif [ -z "$ref" ] \|\| [ "$ref" == "null" ]; then\n ref=${{ github.ref }}\nfi\n\necho "pr_num=$pr_num" >> $GITHUB_ENV\necho "ref=$ref" >> $GITHUB_ENV\n | provenance | | | .github/workflows/test10.yml:20:9:26:6 | Uses Step | .github/workflows/test10.yml:27:14:27:59 | cat foo/.github/java-config.env >> $GITHUB_ENV | provenance | | +| .github/workflows/test11.yml:39:9:47:6 | Uses Step | .github/workflows/test11.yml:49:14:54:29 | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | provenance | | +| .github/workflows/test11.yml:39:9:47:6 | Uses Step | .github/workflows/test11.yml:56:9:62:6 | Uses Step | provenance | | +| .github/workflows/test11.yml:39:9:47:6 | Uses Step | .github/workflows/test11.yml:64:14:69:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | provenance | | +| .github/workflows/test11.yml:56:9:62:6 | Uses Step | .github/workflows/test11.yml:64:14:69:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | provenance | | +| .github/workflows/test12.yml:38:9:46:6 | Uses Step | .github/workflows/test12.yml:48:14:53:29 | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | provenance | | +| .github/workflows/test12.yml:38:9:46:6 | Uses Step | .github/workflows/test12.yml:55:9:61:6 | Uses Step | provenance | | +| .github/workflows/test12.yml:38:9:46:6 | Uses Step | .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | provenance | | +| .github/workflows/test12.yml:55:9:61:6 | Uses Step | .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | provenance | | nodes | .github/workflows/test2.yml:12:9:41:6 | Uses Step | semmle.label | Uses Step | | .github/workflows/test2.yml:41:14:43:52 | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | semmle.label | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | @@ -61,6 +69,14 @@ nodes | .github/workflows/test9.yml:29:14:41:41 | pr_num=$(jq -r '.pull_request.number' artifacts/event_file/event.json)\nif [ -z "$pr_num" ] \|\| [ "$pr_num" == "null" ]; then\n pr_num=""\nfi\n\nref=$pr_num\nif [ -z "$ref" ] \|\| [ "$ref" == "null" ]; then\n ref=${{ github.ref }}\nfi\n\necho "pr_num=$pr_num" >> $GITHUB_ENV\necho "ref=$ref" >> $GITHUB_ENV\n | semmle.label | pr_num=$(jq -r '.pull_request.number' artifacts/event_file/event.json)\nif [ -z "$pr_num" ] \|\| [ "$pr_num" == "null" ]; then\n pr_num=""\nfi\n\nref=$pr_num\nif [ -z "$ref" ] \|\| [ "$ref" == "null" ]; then\n ref=${{ github.ref }}\nfi\n\necho "pr_num=$pr_num" >> $GITHUB_ENV\necho "ref=$ref" >> $GITHUB_ENV\n | | .github/workflows/test10.yml:20:9:26:6 | Uses Step | semmle.label | Uses Step | | .github/workflows/test10.yml:27:14:27:59 | cat foo/.github/java-config.env >> $GITHUB_ENV | semmle.label | cat foo/.github/java-config.env >> $GITHUB_ENV | +| .github/workflows/test11.yml:39:9:47:6 | Uses Step | semmle.label | Uses Step | +| .github/workflows/test11.yml:49:14:54:29 | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | semmle.label | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | +| .github/workflows/test11.yml:56:9:62:6 | Uses Step | semmle.label | Uses Step | +| .github/workflows/test11.yml:64:14:69:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | semmle.label | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | +| .github/workflows/test12.yml:38:9:46:6 | Uses Step | semmle.label | Uses Step | +| .github/workflows/test12.yml:48:14:53:29 | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | semmle.label | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | +| .github/workflows/test12.yml:55:9:61:6 | Uses Step | semmle.label | Uses Step | +| .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | semmle.label | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | subpaths #select | .github/workflows/test2.yml:41:14:43:52 | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | .github/workflows/test2.yml:12:9:41:6 | Uses Step | .github/workflows/test2.yml:41:14:43:52 | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test2.yml:41:14:43:52 | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | @@ -84,3 +100,6 @@ subpaths | .github/workflows/test8.yml:40:14:41:79 | echo "foo=$(< /artifacts/parent-artifacts/event.txt)" >> $GITHUB_ENV\n | .github/workflows/test8.yml:26:9:32:6 | Uses Step | .github/workflows/test8.yml:40:14:41:79 | echo "foo=$(< /artifacts/parent-artifacts/event.txt)" >> $GITHUB_ENV\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test8.yml:40:14:41:79 | echo "foo=$(< /artifacts/parent-artifacts/event.txt)" >> $GITHUB_ENV\n | echo "foo=$(< /artifacts/parent-artifacts/event.txt)" >> $GITHUB_ENV\n | | .github/workflows/test9.yml:29:14:41:41 | pr_num=$(jq -r '.pull_request.number' artifacts/event_file/event.json)\nif [ -z "$pr_num" ] \|\| [ "$pr_num" == "null" ]; then\n pr_num=""\nfi\n\nref=$pr_num\nif [ -z "$ref" ] \|\| [ "$ref" == "null" ]; then\n ref=${{ github.ref }}\nfi\n\necho "pr_num=$pr_num" >> $GITHUB_ENV\necho "ref=$ref" >> $GITHUB_ENV\n | .github/workflows/test9.yml:19:9:27:6 | Uses Step | .github/workflows/test9.yml:29:14:41:41 | pr_num=$(jq -r '.pull_request.number' artifacts/event_file/event.json)\nif [ -z "$pr_num" ] \|\| [ "$pr_num" == "null" ]; then\n pr_num=""\nfi\n\nref=$pr_num\nif [ -z "$ref" ] \|\| [ "$ref" == "null" ]; then\n ref=${{ github.ref }}\nfi\n\necho "pr_num=$pr_num" >> $GITHUB_ENV\necho "ref=$ref" >> $GITHUB_ENV\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test9.yml:29:14:41:41 | pr_num=$(jq -r '.pull_request.number' artifacts/event_file/event.json)\nif [ -z "$pr_num" ] \|\| [ "$pr_num" == "null" ]; then\n pr_num=""\nfi\n\nref=$pr_num\nif [ -z "$ref" ] \|\| [ "$ref" == "null" ]; then\n ref=${{ github.ref }}\nfi\n\necho "pr_num=$pr_num" >> $GITHUB_ENV\necho "ref=$ref" >> $GITHUB_ENV\n | pr_num=$(jq -r '.pull_request.number' artifacts/event_file/event.json)\nif [ -z "$pr_num" ] \|\| [ "$pr_num" == "null" ]; then\n pr_num=""\nfi\n\nref=$pr_num\nif [ -z "$ref" ] \|\| [ "$ref" == "null" ]; then\n ref=${{ github.ref }}\nfi\n\necho "pr_num=$pr_num" >> $GITHUB_ENV\necho "ref=$ref" >> $GITHUB_ENV\n | | .github/workflows/test10.yml:27:14:27:59 | cat foo/.github/java-config.env >> $GITHUB_ENV | .github/workflows/test10.yml:20:9:26:6 | Uses Step | .github/workflows/test10.yml:27:14:27:59 | cat foo/.github/java-config.env >> $GITHUB_ENV | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test10.yml:27:14:27:59 | cat foo/.github/java-config.env >> $GITHUB_ENV | cat foo/.github/java-config.env >> $GITHUB_ENV | +| .github/workflows/test12.yml:48:14:53:29 | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | .github/workflows/test12.yml:38:9:46:6 | Uses Step | .github/workflows/test12.yml:48:14:53:29 | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test12.yml:48:14:53:29 | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | +| .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | .github/workflows/test12.yml:38:9:46:6 | Uses Step | .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | +| .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | .github/workflows/test12.yml:55:9:61:6 | Uses Step | .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | diff --git a/ql/test/query-tests/Security/CWE-077/EnvVarInjectionMedium.expected b/ql/test/query-tests/Security/CWE-077/EnvVarInjectionMedium.expected index eaa9fed4c61..e780af4107d 100644 --- a/ql/test/query-tests/Security/CWE-077/EnvVarInjectionMedium.expected +++ b/ql/test/query-tests/Security/CWE-077/EnvVarInjectionMedium.expected @@ -20,6 +20,14 @@ edges | .github/workflows/test8.yml:26:9:32:6 | Uses Step | .github/workflows/test8.yml:40:14:41:79 | echo "foo=$(< /artifacts/parent-artifacts/event.txt)" >> $GITHUB_ENV\n | provenance | | | .github/workflows/test9.yml:19:9:27:6 | Uses Step | .github/workflows/test9.yml:29:14:41:41 | pr_num=$(jq -r '.pull_request.number' artifacts/event_file/event.json)\nif [ -z "$pr_num" ] \|\| [ "$pr_num" == "null" ]; then\n pr_num=""\nfi\n\nref=$pr_num\nif [ -z "$ref" ] \|\| [ "$ref" == "null" ]; then\n ref=${{ github.ref }}\nfi\n\necho "pr_num=$pr_num" >> $GITHUB_ENV\necho "ref=$ref" >> $GITHUB_ENV\n | provenance | | | .github/workflows/test10.yml:20:9:26:6 | Uses Step | .github/workflows/test10.yml:27:14:27:59 | cat foo/.github/java-config.env >> $GITHUB_ENV | provenance | | +| .github/workflows/test11.yml:39:9:47:6 | Uses Step | .github/workflows/test11.yml:49:14:54:29 | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | provenance | | +| .github/workflows/test11.yml:39:9:47:6 | Uses Step | .github/workflows/test11.yml:56:9:62:6 | Uses Step | provenance | | +| .github/workflows/test11.yml:39:9:47:6 | Uses Step | .github/workflows/test11.yml:64:14:69:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | provenance | | +| .github/workflows/test11.yml:56:9:62:6 | Uses Step | .github/workflows/test11.yml:64:14:69:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | provenance | | +| .github/workflows/test12.yml:38:9:46:6 | Uses Step | .github/workflows/test12.yml:48:14:53:29 | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | provenance | | +| .github/workflows/test12.yml:38:9:46:6 | Uses Step | .github/workflows/test12.yml:55:9:61:6 | Uses Step | provenance | | +| .github/workflows/test12.yml:38:9:46:6 | Uses Step | .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | provenance | | +| .github/workflows/test12.yml:55:9:61:6 | Uses Step | .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | provenance | | nodes | .github/workflows/test2.yml:12:9:41:6 | Uses Step | semmle.label | Uses Step | | .github/workflows/test2.yml:41:14:43:52 | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | semmle.label | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | @@ -61,5 +69,13 @@ nodes | .github/workflows/test9.yml:29:14:41:41 | pr_num=$(jq -r '.pull_request.number' artifacts/event_file/event.json)\nif [ -z "$pr_num" ] \|\| [ "$pr_num" == "null" ]; then\n pr_num=""\nfi\n\nref=$pr_num\nif [ -z "$ref" ] \|\| [ "$ref" == "null" ]; then\n ref=${{ github.ref }}\nfi\n\necho "pr_num=$pr_num" >> $GITHUB_ENV\necho "ref=$ref" >> $GITHUB_ENV\n | semmle.label | pr_num=$(jq -r '.pull_request.number' artifacts/event_file/event.json)\nif [ -z "$pr_num" ] \|\| [ "$pr_num" == "null" ]; then\n pr_num=""\nfi\n\nref=$pr_num\nif [ -z "$ref" ] \|\| [ "$ref" == "null" ]; then\n ref=${{ github.ref }}\nfi\n\necho "pr_num=$pr_num" >> $GITHUB_ENV\necho "ref=$ref" >> $GITHUB_ENV\n | | .github/workflows/test10.yml:20:9:26:6 | Uses Step | semmle.label | Uses Step | | .github/workflows/test10.yml:27:14:27:59 | cat foo/.github/java-config.env >> $GITHUB_ENV | semmle.label | cat foo/.github/java-config.env >> $GITHUB_ENV | +| .github/workflows/test11.yml:39:9:47:6 | Uses Step | semmle.label | Uses Step | +| .github/workflows/test11.yml:49:14:54:29 | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | semmle.label | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | +| .github/workflows/test11.yml:56:9:62:6 | Uses Step | semmle.label | Uses Step | +| .github/workflows/test11.yml:64:14:69:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | semmle.label | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | +| .github/workflows/test12.yml:38:9:46:6 | Uses Step | semmle.label | Uses Step | +| .github/workflows/test12.yml:48:14:53:29 | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | semmle.label | {\n echo 'RUNTIME_VERSIONS<> "$GITHUB_ENV"\n | +| .github/workflows/test12.yml:55:9:61:6 | Uses Step | semmle.label | Uses Step | +| .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | semmle.label | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | subpaths #select diff --git a/ql/test/query-tests/Security/CWE-078/.github/actions/run-airbyte-ci/action.yaml b/ql/test/query-tests/Security/CWE-078/.github/actions/run-airbyte-ci/action.yaml new file mode 100644 index 00000000000..d87c3cad006 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-078/.github/actions/run-airbyte-ci/action.yaml @@ -0,0 +1,196 @@ +name: "Run Dagger pipeline" +description: "Runs a given dagger pipeline" +inputs: + subcommand: + description: "Subcommand for airbyte-ci" + required: true + context: + description: "CI context (e.g., pull_request, manual)" + required: true + github_token: + description: "GitHub token" + required: false + dagger_cloud_token: + description: "Dagger Cloud token" + required: false + docker_hub_username: + description: "Dockerhub username" + required: false + docker_hub_password: + description: "Dockerhub password" + required: false + options: + description: "Options for the subcommand" + required: false + production: + description: "Whether to run in production mode" + required: false + default: "True" + report_bucket_name: + description: "Bucket name for CI reports" + required: false + default: "airbyte-ci-reports-multi" + gcp_gsm_credentials: + description: "GCP credentials for GCP Secret Manager" + required: false + default: "" + gcp_integration_tester_credentials: + description: "GCP credentials for integration tests" + required: false + default: "" + git_repo_url: + description: "Git repository URL" + default: https://github.com/airbytehq/airbyte.git + required: false + git_branch: + description: "Git branch to checkout" + required: false + git_revision: + description: "Git revision to checkout" + required: false + slack_webhook_url: + description: "Slack webhook URL" + required: false + metadata_service_gcs_credentials: + description: "GCP credentials for metadata service" + required: false + metadata_service_bucket_name: + description: "Bucket name for metadata service" + required: false + default: "prod-airbyte-cloud-connector-metadata-service" + sentry_dsn: + description: "Sentry DSN" + required: false + spec_cache_bucket_name: + description: "Bucket name for GCS spec cache" + required: false + default: "io-airbyte-cloud-spec-cache" + spec_cache_gcs_credentials: + description: "GCP credentials for GCS spec cache" + required: false + gcs_credentials: + description: "GCP credentials for GCS" + required: false + ci_job_key: + description: "CI job key" + required: false + s3_build_cache_access_key_id: + description: "Gradle S3 Build Cache AWS access key ID" + required: false + s3_build_cache_secret_key: + description: "Gradle S3 Build Cache AWS secret key" + required: false + airbyte_ci_binary_url: + description: "URL to airbyte-ci binary" + required: false + default: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci + python_registry_token: + description: "Python registry API token to publish python package" + required: false + is_fork: + description: "Whether the PR is from a fork" + required: false + default: "false" + max_attempts: + description: "Number of attempts at running the airbyte-ci command" + required: false + default: 1 + retry_wait_seconds: + description: "Number of seconds to wait between retry attempts" + required: false + default: 60 + +runs: + using: "composite" + steps: + - name: Get start timestamp + id: get-start-timestamp + shell: bash + run: echo "start-timestamp=$(date +%s)" >> $GITHUB_OUTPUT + - name: Docker login + id: docker-login + uses: docker/login-action@v3 + if: ${{ inputs.docker_hub_username != '' && inputs.docker_hub_password != '' }} + with: + username: ${{ inputs.docker_hub_username }} + password: ${{ inputs.docker_hub_password }} + - name: Install Airbyte CI + id: install-airbyte-ci + uses: ./.github/actions/install-airbyte-ci + with: + airbyte_ci_binary_url: ${{ inputs.airbyte_ci_binary_url }} + is_fork: ${{ inputs.is_fork }} + - name: Run airbyte-ci + id: run-airbyte-ci + uses: nick-fields/retry@v3 + env: + CI: "True" + CI_GIT_USER: ${{ github.repository_owner }} + CI_PIPELINE_START_TIMESTAMP: ${{ steps.get-start-timestamp.outputs.start-timestamp }} + PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + # Next environment variables are workflow inputs based and can be set with empty values if the inputs are not required and passed + CI_CONTEXT: "${{ inputs.context }}" + CI_GIT_BRANCH: ${{ inputs.git_branch || github.head_ref }} + CI_GIT_REPO_URL: ${{ inputs.git_repo_url }} + CI_GIT_REVISION: ${{ inputs.git_revision || github.sha }} + CI_GITHUB_ACCESS_TOKEN: ${{ inputs.github_token }} + CI_JOB_KEY: ${{ inputs.ci_job_key }} + CI_REPORT_BUCKET_NAME: ${{ inputs.report_bucket_name }} + DAGGER_CLOUD_TOKEN: "${{ inputs.dagger_cloud_token }}" + DOCKER_HUB_PASSWORD: ${{ inputs.docker_hub_password }} + DOCKER_HUB_USERNAME: ${{ inputs.docker_hub_username }} + GCP_GSM_CREDENTIALS: ${{ inputs.gcp_gsm_credentials }} + GCP_INTEGRATION_TESTER_CREDENTIALS: ${{ inputs.gcp_integration_tester_credentials }} + GCS_CREDENTIALS: ${{ inputs.gcs_credentials }} + METADATA_SERVICE_BUCKET_NAME: ${{ inputs.metadata_service_bucket_name }} + METADATA_SERVICE_GCS_CREDENTIALS: ${{ inputs.metadata_service_gcs_credentials }} + PRODUCTION: ${{ inputs.production }} + PYTHON_REGISTRY_TOKEN: ${{ inputs.python_registry_token }} + PYTHON_REGISTRY_URL: ${{ inputs.python_registry_url }} + S3_BUILD_CACHE_ACCESS_KEY_ID: ${{ inputs.s3_build_cache_access_key_id }} + S3_BUILD_CACHE_SECRET_KEY: ${{ inputs.s3_build_cache_secret_key }} + SENTRY_DSN: ${{ inputs.sentry_dsn }} + SLACK_WEBHOOK: ${{ inputs.slack_webhook_url }} + SPEC_CACHE_BUCKET_NAME: ${{ inputs.spec_cache_bucket_name }} + SPEC_CACHE_GCS_CREDENTIALS: ${{ inputs.spec_cache_gcs_credentials }} + with: + shell: bash + max_attempts: ${{ inputs.max_attempts }} + retry_wait_seconds: ${{ inputs.retry_wait_seconds }} + # 360mn > 6 hours: it's the GitHub runner max job duration + timeout_minutes: 360 + command: | + airbyte-ci --disable-update-check --disable-dagger-run --is-ci --gha-workflow-run-id=${{ github.run_id }} ${{ inputs.subcommand }} ${{ inputs.options }} + - name: Stop Engine + id: stop-engine + if: always() + shell: bash + run: | + mapfile -t containers < <(docker ps --filter name="dagger-engine-*" -q) + if [[ "${#containers[@]}" -gt 0 ]]; then + # give 5mn to the Dagger Engine to push cache data to Dagger Cloud + docker stop -t 300 "${containers[@]}"; + fi + + - name: Collect dagger engine logs + id: collect-dagger-engine-logs + if: always() + uses: jwalton/gh-docker-logs@v2 + with: + dest: "./dagger_engine_logs" + images: "registry.dagger.io/engine" + + - name: Tar logs + id: tar-logs + if: always() + shell: bash + run: tar cvzf ./dagger_engine_logs.tgz ./dagger_engine_logs + + - name: Upload logs to GitHub + id: upload-dagger-engine-logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: ${{ github.job }}_dagger_engine_logs.tgz + path: ./dagger_engine_logs.tgz + retention-days: 7 diff --git a/ql/test/query-tests/Security/CWE-078/.github/workflows/test1.yml b/ql/test/query-tests/Security/CWE-078/.github/workflows/test1.yml new file mode 100644 index 00000000000..6a449e24cf0 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-078/.github/workflows/test1.yml @@ -0,0 +1,63 @@ +name: Finalize connector rollout + +on: + repository_dispatch: + types: [finalize-connector-rollout] + workflow_dispatch: + inputs: + connector_name: + description: "Connector name" + required: true + action: + description: "Action to perform" + required: true + options: ["promote", "rollback"] +jobs: + finalize_rollout: + name: Finalize connector rollout + runs-on: connector-publish-large + env: + ACTION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.action || github.event.client_payload.action }} + steps: + - name: Check action value + run: | + if [[ "${ACTION}" != "promote" && "${ACTION}" != "rollback" ]]; then + echo "Invalid action: ${ACTION}" + exit 1 + fi + shell: bash + - name: Checkout Airbyte + uses: actions/checkout@v4 + - name: Promote {{ github.event.client_payload.connector_name }} release candidate + id: promote-release-candidate + if: ${{ env.ACTION == 'promote' }} + uses: ./.github/actions/run-airbyte-ci + with: + context: "manual" + dagger_cloud_token: ${{ secrets.DAGGER_CLOUD_TOKEN_2 }} + docker_hub_password: ${{ secrets.DOCKER_HUB_PASSWORD }} + docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }} + gcp_gsm_credentials: ${{ secrets.GCP_GSM_CREDENTIALS }} + gcs_credentials: ${{ secrets.METADATA_SERVICE_PROD_GCS_CREDENTIALS }} + github_token: ${{ secrets.GITHUB_TOKEN }} + metadata_service_gcs_credentials: ${{ secrets.METADATA_SERVICE_PROD_GCS_CREDENTIALS }} + sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} + slack_webhook_url: ${{ secrets.PUBLISH_ON_MERGE_SLACK_WEBHOOK }} + subcommand: "connectors --name=${{ github.event.client_payload.connector_name }} publish --promote-release-candidate" + - name: Rollback {{ github.event.client_payload.connector_name }} release candidate + id: rollback-release-candidate + if: ${{ env.ACTION == 'rollback' }} + uses: ./.github/actions/run-airbyte-ci + with: + context: "manual" + dagger_cloud_token: ${{ secrets.DAGGER_CLOUD_TOKEN_2 }} + docker_hub_password: ${{ secrets.DOCKER_HUB_PASSWORD }} + docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }} + gcp_gsm_credentials: ${{ secrets.GCP_GSM_CREDENTIALS }} + gcs_credentials: ${{ secrets.METADATA_SERVICE_PROD_GCS_CREDENTIALS }} + github_token: ${{ secrets.GITHUB_TOKEN }} + metadata_service_gcs_credentials: ${{ secrets.METADATA_SERVICE_PROD_GCS_CREDENTIALS }} + sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} + slack_webhook_url: ${{ secrets.PUBLISH_ON_MERGE_SLACK_WEBHOOK }} + spec_cache_gcs_credentials: ${{ secrets.SPEC_CACHE_SERVICE_ACCOUNT_KEY_PUBLISH }} + subcommand: "connectors --name=${{ github.event.client_payload.connector_name }} publish --rollback-release-candidate" diff --git a/ql/test/query-tests/Security/CWE-078/CommandInjectionCritical.expected b/ql/test/query-tests/Security/CWE-078/CommandInjectionCritical.expected index decabad082f..b66822accab 100644 --- a/ql/test/query-tests/Security/CWE-078/CommandInjectionCritical.expected +++ b/ql/test/query-tests/Security/CWE-078/CommandInjectionCritical.expected @@ -1,6 +1,13 @@ edges +| .github/actions/run-airbyte-ci/action.yaml:4:3:4:12 | input subcommand | .github/actions/run-airbyte-ci/action.yaml:163:118:163:141 | inputs.subcommand | provenance | | +| .github/workflows/test1.yml:46:42:46:90 | github.event.client_payload.connector_name | .github/actions/run-airbyte-ci/action.yaml:4:3:4:12 | input subcommand | provenance | | +| .github/workflows/test1.yml:63:42:63:90 | github.event.client_payload.connector_name | .github/actions/run-airbyte-ci/action.yaml:4:3:4:12 | input subcommand | provenance | | nodes +| .github/actions/run-airbyte-ci/action.yaml:4:3:4:12 | input subcommand | semmle.label | input subcommand | +| .github/actions/run-airbyte-ci/action.yaml:163:118:163:141 | inputs.subcommand | semmle.label | inputs.subcommand | | .github/workflows/comment_issue.yml:9:26:9:57 | github.event.comment.body | semmle.label | github.event.comment.body | +| .github/workflows/test1.yml:46:42:46:90 | github.event.client_payload.connector_name | semmle.label | github.event.client_payload.connector_name | +| .github/workflows/test1.yml:63:42:63:90 | github.event.client_payload.connector_name | semmle.label | github.event.client_payload.connector_name | subpaths #select | .github/workflows/comment_issue.yml:9:26:9:57 | github.event.comment.body | .github/workflows/comment_issue.yml:9:26:9:57 | github.event.comment.body | .github/workflows/comment_issue.yml:9:26:9:57 | github.event.comment.body | Potential command injection in $@, which may be controlled by an external user. | .github/workflows/comment_issue.yml:9:26:9:57 | github.event.comment.body | ${{ github.event.comment.body }} | diff --git a/ql/test/query-tests/Security/CWE-078/CommandInjectionMedium.expected b/ql/test/query-tests/Security/CWE-078/CommandInjectionMedium.expected index 99ebb1edc05..393dde04f35 100644 --- a/ql/test/query-tests/Security/CWE-078/CommandInjectionMedium.expected +++ b/ql/test/query-tests/Security/CWE-078/CommandInjectionMedium.expected @@ -1,5 +1,14 @@ edges +| .github/actions/run-airbyte-ci/action.yaml:4:3:4:12 | input subcommand | .github/actions/run-airbyte-ci/action.yaml:163:118:163:141 | inputs.subcommand | provenance | | +| .github/workflows/test1.yml:46:42:46:90 | github.event.client_payload.connector_name | .github/actions/run-airbyte-ci/action.yaml:4:3:4:12 | input subcommand | provenance | | +| .github/workflows/test1.yml:63:42:63:90 | github.event.client_payload.connector_name | .github/actions/run-airbyte-ci/action.yaml:4:3:4:12 | input subcommand | provenance | | nodes +| .github/actions/run-airbyte-ci/action.yaml:4:3:4:12 | input subcommand | semmle.label | input subcommand | +| .github/actions/run-airbyte-ci/action.yaml:163:118:163:141 | inputs.subcommand | semmle.label | inputs.subcommand | | .github/workflows/comment_issue.yml:9:26:9:57 | github.event.comment.body | semmle.label | github.event.comment.body | +| .github/workflows/test1.yml:46:42:46:90 | github.event.client_payload.connector_name | semmle.label | github.event.client_payload.connector_name | +| .github/workflows/test1.yml:63:42:63:90 | github.event.client_payload.connector_name | semmle.label | github.event.client_payload.connector_name | subpaths #select +| .github/actions/run-airbyte-ci/action.yaml:163:118:163:141 | inputs.subcommand | .github/workflows/test1.yml:46:42:46:90 | github.event.client_payload.connector_name | .github/actions/run-airbyte-ci/action.yaml:163:118:163:141 | inputs.subcommand | Potential command injection in $@, which may be controlled by an external user. | .github/actions/run-airbyte-ci/action.yaml:163:118:163:141 | inputs.subcommand | ${{ inputs.subcommand }} | +| .github/actions/run-airbyte-ci/action.yaml:163:118:163:141 | inputs.subcommand | .github/workflows/test1.yml:63:42:63:90 | github.event.client_payload.connector_name | .github/actions/run-airbyte-ci/action.yaml:163:118:163:141 | inputs.subcommand | Potential command injection in $@, which may be controlled by an external user. | .github/actions/run-airbyte-ci/action.yaml:163:118:163:141 | inputs.subcommand | ${{ inputs.subcommand }} |