Commit 4c3478c3 authored by Pierre Smeyers's avatar Pierre Smeyers
Browse files

Merge branch 'feature/infersharp' into 'main'

feat: add SAST with InferSharp

Closes #10

See merge request to-be-continuous/dotnet!16
parents cddce4c9 e5dec043
Loading
Loading
Loading
Loading
+26 −0
Original line number Diff line number Diff line
@@ -259,6 +259,32 @@ More info:
* [test coverage](https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/test-coverage/test-coverage-parameters/#csharp) & [test execution](https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/test-coverage/test-execution-parameters/#csharp) parameters
* [external analyzer reports](https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/importing-external-issues/external-analyzer-reports/#external-dotnet-issues)

### `dotnet-infersharp` job

This job runs the static code analysis using [Infer#](https://github.com/microsoft/infersharp). On big projects this analysis can take quite some time and use a good amount of memory and disk space. Set a runner size and timeout fitting for your project.

It uses the following variables:

| Input / Variable | Description | Default value |
| ---------------- | ----------- | ------------- |
| `infersharp-enabled` / `DOTNET_INFERSHARP_ENABLED` | Set to true to enable [Infer#](https://github.com/microsoft/infersharp) static analysis. | `false` |
| `infersharp-image` / `DOTNET_INFERSHARP_IMAGE` | Infersharp Docker image to use for running the tests | `mcr.microsoft.com/infersharp:latest`<br/>[![Trivy Badge](https://to-be-continuous.gitlab.io/doc/secu/trivy-badge-DOTNET_INFERSHARP_IMAGE.svg)](https://to-be-continuous.gitlab.io/doc/secu/trivy-DOTNET_INFERSHARP_IMAGE) |
| `infersharp-opts` / `DOTNET_INFERSHARP_OPTS` | Additional [Infer options](https://fbinfer.com/docs/man-infer-run/#OPTIONS) to pass to the analysis. | `""` (empty) |
| `infersharp-blocklist` / `DOTNET_INFERSHARP_BLOCKLIST` | Space-separated list of partial path patterns to filter findings from SARIF output (e.g., suppress test framework false positives). | `Microsoft.TestPlatform xunit nunit MSTest testhost testlogger` |

**Output artifacts:**

The infer# SARIF report is retained as original, filtered using the block-list, and then converted to GitLab SAST and Code Quality formats for reporting depending upon the available Gitlab features.

The following reports are generated:

| Report         | Format                                                                       | Usage             |
| -------------- | ---------------------------------------------------------------------------- | ----------------- |
| `reports/dotnet-infersharp-*-full.sarif.json` | [OASIS Static Analysis Results Interchange Format](https://www.oasis-open.org/committees/tc_home.php) report | Raw results converted to gitlab |
| `reports/dotnet-infersharp-*.sarif.json` | [OASIS Static Analysis Results Interchange Format](https://www.oasis-open.org/committees/tc_home.php) report | Filtered results converted to gitlab |
| `reports/dotnet-infersharp-*.gitlab-sast.json` | [Gitlab SAST](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/src/sast-report-format.json?ref_type=heads) report format| [GitLab integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportssast) |
| `reports/dotnet-infersharp-*.gitlab-codequality.json` | [Code Climate](https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types) | [GitLab integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportscodequality) |

### `dotnet-sbom` job

This job creates a Software Bill Of Materials (SBOM) for the project, libraries and executables using [CycloneDX cdxgen](https://github.com/CycloneDX/cdxgen).
+24 −0
Original line number Diff line number Diff line
@@ -143,6 +143,30 @@
        }
      ]
    },
    {
      "id": "infersharp",
      "name": "Infer# Static Analysis",
      "description": "Set to true to enable [Infersharp](https://github.com/microsoft/infersharp) analysis. Produces both complete and filtered SARIF reports; filtered version is used for GitLab SAST/Code Quality reporting.",
      "enable_with": "DOTNET_INFERSHARP_ENABLED",
      "variables": [
        {
          "name": "DOTNET_INFERSHARP_IMAGE",
          "description": "Infersharp Docker image to use for running the tests",
          "default": "mcr.microsoft.com/infersharp:latest"
        },
        {
          "name": "DOTNET_INFERSHARP_OPTS",
          "description": "Additional [Infer options](https://fbinfer.com/docs/man-infer-run/#OPTIONS) to pass to the analysis.",
          "advanced": true
        },
        {
          "name": "DOTNET_INFERSHARP_BLOCKLIST",
          "description": "Space-separated list of partial path patterns to filter findings from SARIF output (e.g., suppress test framework false positives).",
          "default": "Microsoft.TestPlatform xunit nunit MSTest testhost testlogger",
          "advanced": true
        }
      ]
    },
    {
      "id": "publish",
      "name": "dotnet publish",
+121 −0
Original line number Diff line number Diff line
@@ -81,6 +81,19 @@ spec:
    sbom-opts:
      description: Additional options to pass to the SBOM generation tool
      default: '--fail-on-error --evidence --deep'
    infersharp-enabled:
      description: Set to true to enable [Infersharp](https://github.com/microsoft/infersharp) analysis.
      type: boolean
      default: false
    infersharp-image:
      description: Infersharp Docker image to use for running the tests
      default: mcr.microsoft.com/infersharp:latest
    infersharp-opts:
      description: Additional [Infer options](https://fbinfer.com/docs/man-infer-run/#OPTIONS) to pass to the analysis.
      default: ""
    infersharp-blocklist:
      description: Space-separated list of partial path patterns to filter findings from SARIF output (e.g., suppress test framework false positives).
      default: "Microsoft.TestPlatform xunit nunit MSTest testhost testlogger"
    package-configuration:
      description: The build configuration to use for packaging (Debug or Release).
      default: Release
@@ -201,6 +214,12 @@ variables:
  # Format
  DOTNET_FORMAT_DISABLED: $[[ inputs.format-disabled ]]

  # Infersharp
  DOTNET_INFERSHARP_ENABLED: $[[ inputs.infersharp-enabled ]]
  DOTNET_INFERSHARP_IMAGE: $[[ inputs.infersharp-image ]]
  DOTNET_INFERSHARP_OPTS: $[[ inputs.infersharp-opts ]]
  DOTNET_INFERSHARP_BLOCKLIST: $[[ inputs.infersharp-blocklist ]]

  # SBOM
  DOTNET_SBOM_DISABLED: $[[ inputs.sbom-disabled ]]
  DOTNET_SBOM_SUPPLIER: $[[ inputs.sbom-supplier ]]
@@ -735,6 +754,7 @@ stages:
      url="https://gitlab.com/ignis-build/sarif-converter/-/releases/permalink/latest/downloads/bin/sarif-converter-linux-$(_dotnet_cpu_architecture)"
      log_info "Downloading sarif-converter tool ... ${url}"
      mkdir -p "$DOTNET_CLI_HOME/bin"
      maybe_install_packages curl
      curl -sSL "$url" -o "$DOTNET_CLI_HOME/bin/sarif"
      chmod +x "$DOTNET_CLI_HOME/bin/sarif"
      # shellcheck disable=SC2086
@@ -1550,6 +1570,81 @@ stages:
    log_info "All artifacts published successfully"
  }

  function _dotnet_filter_sarif_blocklist() {
    local input_sarif="$1"
    local output_sarif="$2"
    local blocklist_patterns="$3"
    maybe_install_packages jq
    
    if [ -z "${blocklist_patterns}" ]; then
      log_info "No blocklist patterns to apply, copying SARIF as-is"
      cp "${input_sarif}" "${output_sarif}"
      return 0
    fi

    # Build regex alternation pattern from space-separated partial paths
    local patterns=""
    for pattern in ${blocklist_patterns}; do
      if [ -z "${patterns}" ]; then
        patterns="${pattern}"
      else
        patterns="${patterns}|${pattern}"
      fi
    done

    log_info "Filtering SARIF with partial path patterns: ${patterns}"
    
    # Count before filtering
    local before_count
    before_count=$(jq '[.runs[].results[] // empty] | length' "${input_sarif}")
    
    # Filter out results where uri contains any blocklist pattern
    jq --arg patterns "${patterns}" '
      .runs[].results |= map(
        select(
          .locations[].physicalLocation.artifactLocation.uri as $uri |
          ($uri | test($patterns)) | not
        )
      )
    ' "${input_sarif}" > "${output_sarif}"
    
    # Count after filtering
    local after_count
    after_count=$(jq '[.runs[].results[] // empty] | length' "${output_sarif}")
    local filtered_count=$((before_count - after_count))
    
    log_info "Infersharp analysis: ${before_count} total findings, ${filtered_count} blocklisted, ${after_count} reported"
  }

  function dotnet_run_infersharp() {
    log_info "Running Infer# static analysis on project: ${DOTNET_PROJECT_DIR}"
    mkdir -p "${CI_PROJECT_DIR}/${DOTNET_PROJECT_DIR}/reports"

    local project_path="${CI_PROJECT_DIR}/${DOTNET_PROJECT_DIR}"

    log_info "/infersharp/run_infersharp.sh ${project_path}"
    /infersharp/run_infersharp.sh "${project_path}" -C "${CI_PROJECT_DIR}/${DOTNET_PROJECT_DIR}"

    # Save original unfiltered SARIF
    cp /infersharp/infer-out/report.sarif "${CI_PROJECT_DIR}/${DOTNET_PROJECT_DIR}/reports/dotnet-infersharp-${DOTNET_PROJECT_NAME}-full.sarif.json"

    # Apply blocklist filtering to create filtered version
    _dotnet_filter_sarif_blocklist \
      "/infersharp/infer-out/report.sarif" \
      "${CI_PROJECT_DIR}/${DOTNET_PROJECT_DIR}/reports/dotnet-infersharp-${DOTNET_PROJECT_NAME}.sarif.json" \
      "${DOTNET_INFERSHARP_BLOCKLIST}"
    sed -i "s|${CI_PROJECT_DIR}/||g" "${CI_PROJECT_DIR}/${DOTNET_PROJECT_DIR}/reports/dotnet-infersharp-${DOTNET_PROJECT_NAME}.sarif.json"
    
    # Convert filtered SARIF to GitLab formats
    log_info "Converting filtered Infer# SARIF report to GitLab SAST and Code Quality formats"
    _dotnet_convert_sarif_reports sast "${CI_PROJECT_DIR}/${DOTNET_PROJECT_DIR}/reports/dotnet-infersharp-${DOTNET_PROJECT_NAME}.sarif.json"

    if ! echo "$GITLAB_FEATURES" | grep -qE "sast_|security_advanced"; then
      log_warn "GitLab security dashboard feature is not enabled. Reporting to codequality."
      _dotnet_convert_sarif_reports codequality "${CI_PROJECT_DIR}/${DOTNET_PROJECT_DIR}/reports/dotnet-infersharp-${DOTNET_PROJECT_NAME}.sarif.json"
    fi
  }

  unscope_variables
  eval_all_secrets

@@ -1673,6 +1768,32 @@ dotnet-sbom:
    - if: '$TBC_SBOM_MODE == "onrelease" && ($CI_COMMIT_REF_NAME =~ $RELEASE_REF || $CI_COMMIT_REF_NAME =~ $INTEG_REF || $CI_COMMIT_REF_NAME =~ $PROD_REF)'
    - when: never

dotnet-infersharp:
  extends: .dotnet-env-base
  stage: test
  image:
    name: mcr.microsoft.com/infersharp:latest
    entrypoint: [""]
  variables:
    DOTNET_REQUIRES_SDK_INSTALL: "false"
  before_script:
    - !reference [.dotnet-scripts]
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
  script:
    - dotnet_run_infersharp
  artifacts:
    reports:
      sast: '${DOTNET_PROJECT_DIR}/reports/dotnet-infersharp-*.gitlab-sast.json'
      codequality: '${DOTNET_PROJECT_DIR}/reports/dotnet-infersharp-*.gitlab-codequality.json'
    expire_in: 1 week
    when: always
    paths:
      - '${DOTNET_PROJECT_DIR}/reports/dotnet-infersharp-*'
  rules:
    - if: '$DOTNET_INFERSHARP_ENABLED != "true"'
      when: never
    - !reference [.test-policy, rules]

dotnet-publish:
  extends: .dotnet-env-base
  stage: publish