|
| 1 | +name: Contrast Code Orchestrator |
1 | 2 |
|
| 3 | +on: |
| 4 | + workflow_call: |
| 5 | + inputs: |
| 6 | + # --- Contrast Platform (required) --- |
| 7 | + contrast_host: |
| 8 | + required: true |
| 9 | + type: string |
| 10 | + description: Contrast Platform base URL |
| 11 | + contrast_org_id: |
| 12 | + required: true |
| 13 | + type: string |
| 14 | + description: Contrast organization UUID |
| 15 | + contrast_app_install_id: |
| 16 | + required: true |
| 17 | + type: string |
| 18 | + description: Contrast GitHub App installation ID |
| 19 | + |
| 20 | + # --- Contrast Platform (optional override) --- |
| 21 | + contrast_application_id: |
| 22 | + required: false |
| 23 | + type: string |
| 24 | + default: '' |
| 25 | + description: Manual override for the Contrast application UUID (skips auto-detection) |
| 26 | + run_triage: |
| 27 | + required: false |
| 28 | + type: boolean |
| 29 | + description: Whether to run contrast-triage feature in SmartScan |
| 30 | + triage_duration: |
| 31 | + required: false |
| 32 | + type: string |
| 33 | + description: Duration to run the contrast-triage tool (e.g. '1m', '5m'). |
| 34 | + |
| 35 | + # --- Repo Context (required) --- |
| 36 | + base_branch: |
| 37 | + required: true |
| 38 | + type: string |
| 39 | + description: Default branch of the repository |
| 40 | + repo_url: |
| 41 | + required: true |
| 42 | + type: string |
| 43 | + description: GitHub repository URL |
| 44 | + repo_name: |
| 45 | + required: true |
| 46 | + type: string |
| 47 | + description: Repository full name (owner/repo) |
| 48 | + |
| 49 | + # --- EA Product Disable Flags --- |
| 50 | + disable_smartscan: |
| 51 | + type: string |
| 52 | + default: 'false' |
| 53 | + description: Set to true to skip SmartScan for this repo |
| 54 | + disable_smartsca: |
| 55 | + type: string |
| 56 | + default: 'false' |
| 57 | + description: Set to true to skip SmartSCA for this repo |
| 58 | + disable_smartfix: |
| 59 | + type: string |
| 60 | + default: 'false' |
| 61 | + description: Set to true to skip SmartFix for this repo |
| 62 | + |
| 63 | + # --- SmartFix Config --- |
| 64 | + build_command: |
| 65 | + description: 'Build command to run before SmartFix' |
| 66 | + type: string |
| 67 | + default: 'echo No build command specified.' |
| 68 | + formatting_command: |
| 69 | + description: 'Format command to run before SmartFix' |
| 70 | + type: string |
| 71 | + default: '' |
| 72 | + max_open_prs: |
| 73 | + description: 'Maximum number of open PRs SmartFix will open' |
| 74 | + type: string |
| 75 | + default: '5' |
| 76 | + enable_full_telemetry: |
| 77 | + description: 'Enable SmartFix full telemetry for debugging' |
| 78 | + type: string |
| 79 | + default: 'true' |
| 80 | + debug_mode: |
| 81 | + description: 'Enable SmartFix debug mode for verbose logging' |
| 82 | + type: string |
| 83 | + default: 'false' |
| 84 | + |
| 85 | + # --- LLM Config --- |
| 86 | + use_contrast_llm: |
| 87 | + description: "Indicates to use Contrast internal LLM" |
| 88 | + type: string |
| 89 | + default: 'true' |
| 90 | + agent_model: |
| 91 | + description: 'The provider LLM model to use' |
| 92 | + type: string |
| 93 | + default: '' |
| 94 | + azure_api_base: |
| 95 | + description: 'The Azure API base URL' |
| 96 | + type: string |
| 97 | + default: '' |
| 98 | + azure_api_version: |
| 99 | + description: 'The Azure API version' |
| 100 | + type: string |
| 101 | + default: '' |
| 102 | + use_aws: |
| 103 | + description: 'Whether to use AWS credentials configuration' |
| 104 | + type: string |
| 105 | + default: 'false' |
| 106 | + aws_region: |
| 107 | + description: 'The AWS region for Bedrock LLM' |
| 108 | + type: string |
| 109 | + default: '' |
| 110 | + |
| 111 | + # --- Runner --- |
| 112 | + github_runner: |
| 113 | + description: 'The GitHub runner to use (example: ubuntu-latest)' |
| 114 | + type: string |
| 115 | + default: 'ubuntu-latest' |
| 116 | + |
| 117 | + secrets: |
| 118 | + CONTRAST_API_KEY: |
| 119 | + description: The contrast API key |
| 120 | + required: true |
| 121 | + CONTRAST_AUTHORIZATION_KEY: |
| 122 | + description: The contrast authorization key |
| 123 | + required: true |
| 124 | + GH_TOKEN: |
| 125 | + description: The GitHub token |
| 126 | + required: true |
| 127 | + ANTHROPIC_API_KEY: |
| 128 | + description: The Anthropic API key |
| 129 | + required: false |
| 130 | + GEMINI_API_KEY: |
| 131 | + description: The Google Gemini API key |
| 132 | + required: false |
| 133 | + AZURE_API_KEY: |
| 134 | + description: The Azure API key |
| 135 | + required: false |
| 136 | + AWS_BEARER_TOKEN_BEDROCK: |
| 137 | + description: The AWS Bedrock API key |
| 138 | + required: false |
| 139 | + AWS_ACCESS_KEY_ID: |
| 140 | + description: The AWS access key id |
| 141 | + required: false |
| 142 | + AWS_SECRET_ACCESS_KEY: |
| 143 | + description: The AWS secret access key |
| 144 | + required: false |
| 145 | + AWS_SESSION_TOKEN: |
| 146 | + description: The AWS session token |
| 147 | + required: false |
| 148 | + AWS_ROLE_TO_ASSUME: |
| 149 | + description: The AWS role to assume |
| 150 | + required: false |
| 151 | + |
| 152 | +permissions: |
| 153 | + contents: write |
| 154 | + pull-requests: write |
| 155 | + id-token: write |
| 156 | + |
| 157 | +jobs: |
| 158 | + |
| 159 | + # ───────────────────────────────────────────────────────────────────── |
| 160 | + # JOB 1: Detect Contrast Application Data |
| 161 | + # Runs contrast/module-identifier:v1 to resolve the Contrast app ID/Name |
| 162 | + # for this repository. The container writes CONTRAST_APP_ID / CONTRAST_APP_Name |
| 163 | + # to $GITHUB_ENV. The validate step forwards it to $GITHUB_OUTPUT so |
| 164 | + # downstream jobs can consume it. |
| 165 | + # ───────────────────────────────────────────────────────────────────── |
| 166 | + detect-app-data: |
| 167 | + name: Detect Contrast Application Data |
| 168 | + runs-on: ${{ inputs.github_runner }} |
| 169 | + outputs: |
| 170 | + contrast_app_id: ${{ steps.set-app-id-output.outputs.contrast_app_id }} |
| 171 | + contrast_app_name: ${{ steps.set-app-name-output.outputs.contrast_app_name }} |
| 172 | + steps: |
| 173 | + - name: Checkout Repository |
| 174 | + uses: actions/checkout@v4 |
| 175 | + |
| 176 | + #- name: Configure AWS Credentials |
| 177 | + #if: ${{ inputs.use_aws == 'true' && inputs.aws_region != '' }} |
| 178 | + #uses: aws-actions/configure-aws-credentials@v5 |
| 179 | + #with: |
| 180 | + #aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} |
| 181 | + #aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} |
| 182 | + #aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }} |
| 183 | + #role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} |
| 184 | + #aws-region: ${{ inputs.aws_region }} |
| 185 | + |
| 186 | + - name: Print workspace path |
| 187 | + run: | |
| 188 | + echo "GitHub workspace path: ${{ github.workspace }}" |
| 189 | + echo "GITHUB_WORKSPACE env var: $GITHUB_WORKSPACE" |
| 190 | +
|
| 191 | + - name: Run Contrast Module Identifier |
| 192 | + uses: docker://contrast/module-identifier |
| 193 | + with: |
| 194 | + args: "$GITHUB_WORKSPACE" --single --debug |
| 195 | + env: |
| 196 | + CONTRAST_HOST_NAME: ${{ inputs.contrast_host }} |
| 197 | + CONTRAST_ORG_ID: ${{ inputs.contrast_org_id }} |
| 198 | + CONTRAST_API_KEY: ${{ secrets.CONTRAST_API_KEY }} |
| 199 | + CONTRAST_AUTH_TOKEN: ${{ secrets.CONTRAST_AUTHORIZATION_KEY }} |
| 200 | + |
| 201 | + - name: Validate Application ID and Set Output |
| 202 | + id: set-app-id-output |
| 203 | + shell: bash |
| 204 | + run: | |
| 205 | + if [[ -n "$CONTRAST_APP_ID" && "$CONTRAST_APP_ID" != "null" ]]; then |
| 206 | + echo "Successfully detected contrast application ID: $CONTRAST_APP_ID" |
| 207 | + elif [[ -n "${{ inputs.contrast_application_id }}" ]]; then |
| 208 | + echo "Auto-detection did not find an contrast application ID. Using contrast_application_id input override." |
| 209 | + CONTRAST_APP_ID="${{ inputs.contrast_application_id }}" |
| 210 | + echo "CONTRAST_APP_ID=$CONTRAST_APP_ID" >> $GITHUB_ENV |
| 211 | + else |
| 212 | + echo "Contrast application ID could not be automatically detected for repository '${{ inputs.repo_name }}'." |
| 213 | + echo "" |
| 214 | + echo "Set CONTRAST_APP_ID manually:" |
| 215 | + echo " 1. Browse your applications: ${{ inputs.contrast_host }}/Contrast/static/ng/index.html#/${{ inputs.contrast_org_id }}/applications" |
| 216 | + echo " 2. Select your application" |
| 217 | + echo " 3. The ID is the UUID after applications/ in the URL" |
| 218 | + echo " e.g. applications/057254e6-f065-40fa-8197-e1d7ace10e67" |
| 219 | + fi |
| 220 | +
|
| 221 | + if [[ -n "$CONTRAST_APP_ID" && "$CONTRAST_APP_ID" != "null" ]]; then |
| 222 | + echo "contrast_app_id=$CONTRAST_APP_ID" >> $GITHUB_OUTPUT |
| 223 | + fi |
| 224 | +
|
| 225 | + - name: Validate Application Name and Set Output |
| 226 | + id: set-app-name-output |
| 227 | + shell: bash |
| 228 | + run: | |
| 229 | + if [[ -z "$CONTRAST_APP_NAME" || "$CONTRAST_APP_NAME" == "null" ]]; then |
| 230 | + echo "Contrast application name could not be automatically detected for repository '${{ inputs.repo_name }}'." |
| 231 | + else |
| 232 | + echo "Successfully detected application name: $CONTRAST_APP_NAME" |
| 233 | + echo "contrast_app_name=$CONTRAST_APP_NAME" >> $GITHUB_OUTPUT |
| 234 | + fi |
| 235 | +
|
| 236 | + # ───────────────────────────────────────────────────────────────────── |
| 237 | + # JOB 2: Run SmartScan |
| 238 | + # SAST scanner — runs on every trigger event. |
| 239 | + # Skipped if disable_smartscan is 'true'. |
| 240 | + # Uses contrast/smartscan:v1 public DockerHub image — no credentials required. |
| 241 | + # ───────────────────────────────────────────────────────────────────── |
| 242 | + run-contrast-code: |
| 243 | + name: Run Contrast Code |
| 244 | + needs: detect-app-data |
| 245 | + if: ${{ inputs.disable_smartscan != 'true' }} |
| 246 | + runs-on: ${{ inputs.github_runner }} |
| 247 | + steps: |
| 248 | + - name: Checkout Repository |
| 249 | + uses: actions/checkout@v4 |
| 250 | + |
| 251 | + - name: Configure AWS Credentials |
| 252 | + if: ${{ inputs.use_aws == 'true' && inputs.aws_region != '' }} |
| 253 | + uses: aws-actions/configure-aws-credentials@v5 |
| 254 | + with: |
| 255 | + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} |
| 256 | + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} |
| 257 | + aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }} |
| 258 | + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} |
| 259 | + aws-region: ${{ inputs.aws_region }} |
| 260 | + |
| 261 | + - name: Get Repository Languages |
| 262 | + id: get_repo_langs |
| 263 | + uses: octokit/request-action@v2.4.0 |
| 264 | + with: |
| 265 | + route: GET /repos/${{ inputs.repo_name }}/languages |
| 266 | + env: |
| 267 | + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} |
| 268 | + |
| 269 | + - name: Set Repo Language Info |
| 270 | + run: | |
| 271 | + echo '${{ steps.get_repo_langs.outputs.data }}' \ |
| 272 | + | jq -r 'to_entries[] | [.[]] | join(",")' \ |
| 273 | + | tr '\n' '|' \ |
| 274 | + | xargs -I '{}' echo "REPO_LANG_INFO={}" >> $GITHUB_ENV |
| 275 | +
|
| 276 | + - name: Set Branch Name |
| 277 | + run: | |
| 278 | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then |
| 279 | + echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV |
| 280 | + else |
| 281 | + echo "BRANCH_NAME=${{ github.ref_name }}" >> $GITHUB_ENV |
| 282 | + fi |
| 283 | +
|
| 284 | + - name: Run Contrast Code Scanner |
| 285 | + uses: docker://contrast/sast-smartai |
| 286 | + env: |
| 287 | + LOGLEVEL: 'DEBUG' |
| 288 | + REPO_NAME: ${{ github.event.repository.name }} |
| 289 | + REPO_LANG_INFO: ${{ env.REPO_LANG_INFO }} |
| 290 | + REPO_HTTP_URL: ${{ inputs.repo_url }}/${{ inputs.repo_name }} |
| 291 | + BRANCH_NAME: ${{ env.BRANCH_NAME }} |
| 292 | + DEFAULT_BRANCH: ${{ inputs.base_branch }} |
| 293 | + APPLICATION_NAME: ${{ needs.detect-app-name.outputs.contrast_app_name }} |
| 294 | + COMMIT_HASH: ${{ github.sha }} |
| 295 | + COMMITTER: ${{ github.triggering_actor }} |
| 296 | + TAG: ${{ github.ref }} |
| 297 | + RUN_ID: ${{ github.run_id }} |
| 298 | + WORKFLOW_ID: ${{ github.workflow_sha }} |
| 299 | + CONTRAST_BASE_URL: ${{ inputs.contrast_host }} |
| 300 | + CONTRAST_ORG_ID: ${{ inputs.contrast_org_id }} |
| 301 | + CONTRAST_API_KEY: ${{ secrets.CONTRAST_API_KEY }} |
| 302 | + CONTRAST_AUTH_TOKEN: ${{ secrets.CONTRAST_AUTHORIZATION_KEY }} |
| 303 | + RUN_TRIAGE: ${{ inputs.run_triage || True }} |
| 304 | + TRIAGE_DURATION: ${{ inputs.triage_duration || '' }} |
| 305 | + APPLICATION_DIR: "$GITHUB_WORKSPACE" |
| 306 | + |
| 307 | + # ───────────────────────────────────────────────────────────────────── |
| 308 | + # JOB 3: Run SmartSCA |
| 309 | + # SCA dependency scanning. Disabled by default for EA. |
| 310 | + # Will be enabled once contrast/smart-sca:v1 is delivered by the SCA team. |
| 311 | + # ───────────────────────────────────────────────────────────────────── |
| 312 | + run-sca: |
| 313 | + name: Run SmartSCA |
| 314 | + needs: detect-app-data |
| 315 | + if: ${{ inputs.disable_smartsca != 'true' }} |
| 316 | + runs-on: ${{ inputs.github_runner }} |
| 317 | + steps: |
| 318 | + - name: Checkout Repository |
| 319 | + uses: actions/checkout@v4 |
| 320 | + |
| 321 | + - name: Run SmartSCA |
| 322 | + uses: docker://dougjeremias489/contrast-sca-cli |
| 323 | + env: |
| 324 | + CONTRAST_HOST: ${{ inputs.contrast_host }} |
| 325 | + CONTRAST_ORG_ID: ${{ inputs.contrast_org_id }} |
| 326 | + CONTRAST_APP_INSTALLATION_ID: ${{ inputs.contrast_app_install_id }} |
| 327 | + CONTRAST_API_KEY: ${{ secrets.CONTRAST_API_KEY }} |
| 328 | + CONTRAST_AUTHORIZATION: ${{ secrets.CONTRAST_AUTHORIZATION_KEY }} |
| 329 | + GITHUB_SERVER_URL: ${{ inputs.repo_url }} |
| 330 | + GITHUB_REPOSITORY: ${{ inputs.repo_name }} |
| 331 | + |
| 332 | + # ───────────────────────────────────────────────────────────────────── |
| 333 | + # JOB 4: Run SmartFix |
| 334 | + # AI-powered fix PR generation. Event-gated — only runs on: |
| 335 | + # • pull_request (closed) — handles merge notification or PR close |
| 336 | + # • workflow_dispatch — manual trigger to generate fixes |
| 337 | + # • schedule — scheduled fix generation |
| 338 | + # Does NOT run on PR open/synchronize to avoid redundant runs. |
| 339 | + # ───────────────────────────────────────────────────────────────────── |
| 340 | + run-smartfix: |
| 341 | + name: Run SmartFix |
| 342 | + needs: detect-app-data |
| 343 | + if: | |
| 344 | + inputs.disable_smartfix != 'true' && |
| 345 | + ( |
| 346 | + github.event_name == 'workflow_dispatch' || |
| 347 | + github.event_name == 'schedule' || |
| 348 | + (github.event_name == 'pull_request' && github.event.action == 'closed') |
| 349 | + ) |
| 350 | + runs-on: ${{ inputs.github_runner }} |
| 351 | + steps: |
| 352 | + - name: Determine SmartFix Job Type |
| 353 | + id: job_type |
| 354 | + run: | |
| 355 | + if [[ "${{ github.event.pull_request.merged }}" == "true" ]]; then |
| 356 | + echo "label=Notify Contrast on PR Merge" >> $GITHUB_OUTPUT |
| 357 | + elif [[ "${{ github.event.pull_request.merged }}" == "false" ]]; then |
| 358 | + echo "label=Handle PR Close" >> $GITHUB_OUTPUT |
| 359 | + else |
| 360 | + echo "label=Run Contrast AI SmartFix - Generate Fixes" >> $GITHUB_OUTPUT |
| 361 | + fi |
| 362 | +
|
| 363 | + - name: Configure AWS Credentials |
| 364 | + if: ${{ inputs.use_aws == 'true' && inputs.aws_region != '' }} |
| 365 | + uses: aws-actions/configure-aws-credentials@v5 |
| 366 | + with: |
| 367 | + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} |
| 368 | + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} |
| 369 | + aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }} |
| 370 | + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} |
| 371 | + aws-region: ${{ inputs.aws_region }} |
| 372 | + |
| 373 | + - name: ${{ steps.job_type.outputs.label }} |
| 374 | + uses: Contrast-Security-OSS/contrast-ai-smartfix-action@v1 |
| 375 | + with: |
| 376 | + contrast_host: ${{ inputs.contrast_host }} |
| 377 | + contrast_org_id: ${{ inputs.contrast_org_id }} |
| 378 | + contrast_app_id: ${{ needs.detect-app-id.outputs.contrast_app_id || contrast_application_id }} |
| 379 | + base_branch: ${{ inputs.base_branch }} |
| 380 | + build_command: ${{ inputs.build_command }} |
| 381 | + formatting_command: ${{ inputs.formatting_command }} |
| 382 | + use_contrast_llm: ${{ inputs.use_contrast_llm }} |
| 383 | + max_open_prs: ${{ inputs.max_open_prs }} |
| 384 | + agent_model: ${{ inputs.agent_model }} |
| 385 | + azure_api_base: ${{ inputs.azure_api_base }} |
| 386 | + azure_api_version: ${{ inputs.azure_api_version }} |
| 387 | + enable_full_telemetry: ${{ inputs.enable_full_telemetry }} |
| 388 | + debug_mode: ${{ inputs.debug_mode }} |
| 389 | + aws_region: ${{ inputs.aws_region }} |
| 390 | + contrast_api_key: ${{ secrets.CONTRAST_API_KEY }} |
| 391 | + contrast_authorization_key: ${{ secrets.CONTRAST_AUTHORIZATION_KEY }} |
| 392 | + github_token: ${{ secrets.GH_TOKEN }} |
| 393 | + gemini_api_key: ${{ secrets.GEMINI_API_KEY }} |
| 394 | + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} |
| 395 | + azure_api_key: ${{ secrets.AZURE_API_KEY }} |
| 396 | + aws_bearer_token_bedrock: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }} |
0 commit comments