diff --git a/fcli-core/fcli-action/src/main/resources/com/fortify/cli/generic_action/actions/zip/detect-env.yaml b/fcli-core/fcli-action/src/main/resources/com/fortify/cli/generic_action/actions/zip/detect-env.yaml index 416c26b9b94..9ae18e0ced6 100644 --- a/fcli-core/fcli-action/src/main/resources/com/fortify/cli/generic_action/actions/zip/detect-env.yaml +++ b/fcli-core/fcli-action/src/main/resources/com/fortify/cli/generic_action/actions/zip/detect-env.yaml @@ -6,7 +6,7 @@ usage: description: | This action collects data about the current environment in which fcli is being run, like the current CI/CD platform (GitHub, GitLab, Azure DevOps, ...), Git data from - current source code directory, ... Collected data is stored in global.ci.* action + current source code directory, ... Collected data is stored in global.ci.* action variables for use by other actions, and printed to the output for user reference. This includes both environment-specific data, and any data derived from that data, like default FoD release name and SSC application version name. @@ -14,7 +14,7 @@ usage: config: output: immediate mcp: exclude - + steps: # Only run if not run before; global.isCiInitialized is set at the end of this action. # Callers may also check this variable to avoid re-running this action. @@ -26,9 +26,9 @@ steps: global.ci.fcliVersion: ${fcliBuildProps.fcliVersion} # Fcli version global.ci.fcliBuildInfo: ${fcliBuildProps.fcliBuildInfo} # Fcli build information global.ci.name: # Name of current CI system - global.ci.id: # Id of current CI system, used to look up -* actions + global.ci.id: # Id of current CI system, used to look up -* actions global.ci.qualifiedRepoName: # Fully qualified repository name - global.ci.sourceBranch: # The current branch being processed/scanned + global.ci.sourceBranch: # The current branch being processed/scanned global.ci.commitHeadSHA: # Head commit SHA (actual commit on branch) global.ci.commitMergeSHA: # Merge commit SHA (for PRs on GitHub, same as head otherwise) global.ci.workspaceDir: "." # Workspace/repository root directory (default to current dir) @@ -38,6 +38,7 @@ steps: global.ci.prId: # Pull/merge request numeric identifier (null if not active) global.ci.prTarget: # Pull/merge request target branch (null if not active) global.ci.prTerminology: "Pull Request" # Pull/merge request terminology for this CI system (default) + global.ci.prCommentActionSuffix: pr-comment # Comment action suffix, for example pr-comment or mr-comment # The following are set by default at the end, but may be overridden by individual CI configurations global.fod.prCommentAction: # FoD PR comment action global.ssc.prCommentAction: # SSC PR comment action @@ -52,7 +53,7 @@ steps: ci.detected: ${#_ci.detect()} ci.type: ${ci.detected.type} ci.env: ${ci.detected.env} - + # For recognized CI systems (not unknown), extract properties from detected environment # Using conditional navigation operator ?. to safely access properties even if ci.env is empty - if: ${ci.type!='unknown'} @@ -69,12 +70,13 @@ steps: global.ci.prId: ${ci.env?.pullRequest?.id} global.ci.prTarget: ${ci.env?.pullRequest?.target} global.ci.prTerminology: ${ci.env?.prTerminology?:global.ci.prTerminology} - + global.ci.prCommentActionSuffix: ${ci.env?.prCommentActionSuffix?:global.ci.prCommentActionSuffix} + # GitHub-specific properties - if: ${ci.type=='github'} var.set: global.ci.jobSummaryFile: ${#ifBlank(global.ci.jobSummaryFile,ci.env?.jobSummaryFile)} - + # Jenkins - if: ${#isNotBlank(#env('JENKINS_HOME'))||#isNotBlank(#env('JENKINS_URL'))} var.set: @@ -82,7 +84,19 @@ steps: global.ci.id: jenkins global.ci.workspaceDir: ${#env('WORKSPACE')} global.ci.sourceDir: ${#env('WORKSPACE')} - + + # Azure DevOps - detect PR context from @fortify/setup env ado + - if: ${#isNotBlank(#env('SYSTEM_TEAMFOUNDATIONSERVERURI'))} + var.set: + global.ci.name: Azure DevOps + global.ci.id: ado + global.ci.workspaceDir: ${#env('BUILD_SOURCESDIRECTORY')} + global.ci.sourceDir: ${#env('BUILD_SOURCESDIRECTORY')} + global.ci.prActive: ${#isNotBlank(#env('SYSTEM_PULLREQUEST_PULLREQUESTID'))} + global.ci.prId: ${#env('SYSTEM_PULLREQUEST_PULLREQUESTID')} + global.ci.prTarget: ${#env('SYSTEM_PULLREQUEST_TARGETBRANCH')} + global.ci.prCommentActionSuffix: pr-comment + # Override sourceDir with SOURCE_DIR if specified (custom user variable) # NOTE: workspaceDir is NOT overridden by SOURCE_DIR, as it should always be the workspace root - if: ${#isNotBlank(#env('SOURCE_DIR'))} @@ -94,7 +108,7 @@ steps: global.ci.sourceDir: ${#ifBlank(global.ci.sourceDir,'.')} - var.set: global.ci.localRepo: ${#localRepo(global.ci.sourceDir)} - + # Generic local repository fallback (run if previous CI-specific steps didn't set these) - if: ${#isBlank(global.ci.id) && global.ci.localRepo!=null} var.set: @@ -104,14 +118,14 @@ steps: global.ci.sourceBranch: ${global.ci.localRepo.branch?.short} global.ci.commitHeadSHA: ${global.ci.localRepo.commit?.headId?.full} global.ci.commitMergeSHA: ${global.ci.localRepo.commit?.mergeId?.full} - + # Additional generic variables based on the output of the CI-specific sections above - var.set: global.ci.defaultFortifyRepo: ${#joinOrNull(':', global.ci.qualifiedRepoName, global.ci.sourceBranch)} - # Set default reporting actions based on ci identifier. Note that FoD/SSC CI actions should check existence of these actions + # Set default reporting actions based on ci identifier. Note that FoD/SSC CI actions should check existence of these actions # TODO Only use default values if not explicitly defined in CI-specific sections above. - global.ci.fod_prCommentAction: ${#actionOrNull('fod',#joinOrNull('-', global.ci.id, 'pr-comment'))} - global.ci.ssc_prCommentAction: ${#actionOrNull('ssc',#joinOrNull('-', global.ci.id, 'pr-comment'))} + global.ci.fod_prCommentAction: ${#actionOrNull('fod',#joinOrNull('-', global.ci.id, global.ci.prCommentActionSuffix))} + global.ci.ssc_prCommentAction: ${#actionOrNull('ssc',#joinOrNull('-', global.ci.id, global.ci.prCommentActionSuffix))} global.ci.fod_sastExportAction: ${#actionOrNull('fod',#joinOrNull('-', global.ci.id, 'sast-report'))} global.ci.ssc_sastExportAction: ${#actionOrNull('ssc',#joinOrNull('-', global.ci.id, 'sast-report'))} global.ci.fod_dastExportAction: ${#actionOrNull('fod',#joinOrNull('-', global.ci.id, 'dast-report'))} @@ -122,13 +136,13 @@ steps: - if: "${global.ci.prActive!=true}" var.set: global.ci.prNotActiveSkipReason: "Not a ${global.ci.prTerminology}" - - log.info: "${global.ci.name!=null ? 'Detected '+global.ci.name : 'No CI system detected'}" + - log.info: {msg: "${global.ci.name!=null ? 'Detected '+global.ci.name : 'No CI system detected'}"} - records.for-each: from: ${#properties(global.ci)} record.var-name: p do: - if: ${#isDebugEnabled() || p.value!=null} - log.info: "${' '+p.key+': '+p.value}" + log.info: {msg: "${' '+p.key+': '+p.value}"} # Mark as initialized to prevent re-running this action - var.set: diff --git a/fcli-core/fcli-app/src/main/resources/com/fortify/cli/app/actions/build-time/ci-doc.yaml b/fcli-core/fcli-app/src/main/resources/com/fortify/cli/app/actions/build-time/ci-doc.yaml index 2e29abbd104..77476063c1f 100644 --- a/fcli-core/fcli-app/src/main/resources/com/fortify/cli/app/actions/build-time/ci-doc.yaml +++ b/fcli-core/fcli-app/src/main/resources/com/fortify/cli/app/actions/build-time/ci-doc.yaml @@ -256,10 +256,13 @@ formatters: desc: >- (PREVIEW) If `DO_PR_COMMENT` is set to true (implied if any of the other two `PR_COMMENT_*` variables are set), a Pull Request or Merge Request comment will be generated using an fcli-provided action - matching the current CI system like actionRef:_:github-pr-comment or, if specified, the custom fcli action - specified through `PR_COMMENT_ACTION`. Extra options for the fcli action can be specified through - the `PR_COMMENT_EXTRA_OPTS` environment variable, which may include fcli options to allow unsigned - custom actions to be used. + matching the current CI system. For GitHub, Azure DevOps, and Bitbucket the action is named + `-pr-comment` (e.g., actionRef:_:github-pr-comment or actionRef:_:ado-pr-comment). + For GitLab, the action is named `-mr-comment` (e.g., actionRef:_:gitlab-mr-comment) + to reflect GitLab's Merge Request terminology. + A custom action can be specified through `PR_COMMENT_ACTION`. Extra options for the fcli action + can be specified through the `PR_COMMENT_EXTRA_OPTS` environment variable, which may include + fcli options to allow unsigned custom actions to be used. - names: DO_SAST_EXPORT\nSAST_EXPORT_ACTION\nSAST_EXPORT_EXTRA_OPTS desc: >- If `DO_SAST_EXPORT` is not set to `false` and a SAST scan was completed, the SAST vulnerability @@ -413,10 +416,13 @@ formatters: desc: >- (PREVIEW) If `DO_PR_COMMENT` is set to true (implied if any of the other two `PR_COMMENT_*` variables are set), a Pull Request or Merge Request comment will be generated using an fcli-provided action - matching the current CI system like actionRef:_:github-pr-comment or, if specified, the custom fcli action - specified through `PR_COMMENT_ACTION`. Extra options for the fcli action can be specified through - the `PR_COMMENT_EXTRA_OPTS` environment variable, which may include fcli options to allow unsigned - custom actions to be used. + matching the current CI system. For GitHub, Azure DevOps, and Bitbucket the action is named + `-pr-comment` (e.g., actionRef:_:github-pr-comment or actionRef:_:ado-pr-comment). + For GitLab, the action is named `-mr-comment` (e.g., actionRef:_:gitlab-mr-comment) + to reflect GitLab's Merge Request terminology. + A custom action can be specified through `PR_COMMENT_ACTION`. Extra options for the fcli action + can be specified through the `PR_COMMENT_EXTRA_OPTS` environment variable, which may include + fcli options to allow unsigned custom actions to be used. - names: DO_SAST_EXPORT\nSAST_EXPORT_ACTION\nSAST_EXPORT_EXTRA_OPTS desc: >- If `DO_SAST_EXPORT` is not set to `false` and a SAST scan was completed, the SAST vulnerability diff --git a/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/ado/AdoEnvironment.java b/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/ado/AdoEnvironment.java index 9441596ca7a..e4f655fbbea 100644 --- a/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/ado/AdoEnvironment.java +++ b/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/ado/AdoEnvironment.java @@ -49,6 +49,7 @@ public record AdoEnvironment( String repositoryId, String buildId, String prTerminology, + String prCommentActionSuffix, String ciName, String ciId ) { @@ -57,23 +58,39 @@ public record AdoEnvironment( public static final String NAME = "Azure DevOps"; public static final String ID = "ado"; public static final String PR_TERMINOLOGY = "Pull Request"; + public static final String PR_COMMENT_ACTION_SUFFIX = "pr-comment"; // Environment variable names public static final String ENV_ORGANIZATION_URL = "System.TeamFoundationCollectionUri"; + public static final String ENV_ORGANIZATION_URL_ALT = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI"; public static final String ENV_PROJECT = "System.TeamProject"; + public static final String ENV_PROJECT_ALT = "SYSTEM_TEAMPROJECT"; public static final String ENV_REPOSITORY_NAME = "Build.Repository.Name"; + public static final String ENV_REPOSITORY_NAME_ALT = "BUILD_REPOSITORY_NAME"; public static final String ENV_REPOSITORY_ID = "Build.Repository.ID"; + public static final String ENV_REPOSITORY_ID_ALT = "BUILD_REPOSITORY_ID"; public static final String ENV_BUILD_ID = "Build.BuildId"; + public static final String ENV_BUILD_ID_ALT = "BUILD_BUILDID"; public static final String ENV_SOURCE_BRANCH = "Build.SourceBranch"; + public static final String ENV_SOURCE_BRANCH_ALT = "BUILD_SOURCEBRANCH"; public static final String ENV_SOURCE_BRANCH_NAME = "Build.SourceBranchName"; + public static final String ENV_SOURCE_BRANCH_NAME_ALT = "BUILD_SOURCEBRANCHNAME"; public static final String ENV_SOURCE_VERSION = "Build.SourceVersion"; + public static final String ENV_SOURCE_VERSION_ALT = "BUILD_SOURCEVERSION"; public static final String ENV_SOURCES_DIRECTORY = "Build.SourcesDirectory"; + public static final String ENV_SOURCES_DIRECTORY_ALT = "BUILD_SOURCESDIRECTORY"; public static final String ENV_DEFAULT_WORKING_DIRECTORY = "System.DefaultWorkingDirectory"; + public static final String ENV_DEFAULT_WORKING_DIRECTORY_ALT = "SYSTEM_DEFAULTWORKINGDIRECTORY"; public static final String ENV_PR_SOURCE_BRANCH = "System.PullRequest.SourceBranch"; + public static final String ENV_PR_SOURCE_BRANCH_ALT = "SYSTEM_PULLREQUEST_SOURCEBRANCH"; public static final String ENV_PR_SOURCE_BRANCH_NAME = "System.PullRequest.SourceBranchName"; + public static final String ENV_PR_SOURCE_BRANCH_NAME_ALT = "SYSTEM_PULLREQUEST_SOURCEBRANCHNAME"; public static final String ENV_PR_TARGET_BRANCH = "System.PullRequest.TargetBranch"; + public static final String ENV_PR_TARGET_BRANCH_ALT = "SYSTEM_PULLREQUEST_TARGETBRANCH"; public static final String ENV_PR_TARGET_BRANCH_NAME = "System.PullRequest.TargetBranchName"; + public static final String ENV_PR_TARGET_BRANCH_NAME_ALT = "SYSTEM_PULLREQUEST_TARGETBRANCHNAME"; public static final String ENV_PR_ID = "System.PullRequest.PullRequestId"; + public static final String ENV_PR_ID_ALT = "SYSTEM_PULLREQUEST_PULLREQUESTID"; public static final String ENV_TOKEN = "ADO_TOKEN"; /** @@ -81,17 +98,17 @@ public record AdoEnvironment( * Returns null if not running in Azure DevOps. */ public static AdoEnvironment detect() { - var repoName = EnvHelper.env(ENV_REPOSITORY_NAME); + var repoName = env(ENV_REPOSITORY_NAME, ENV_REPOSITORY_NAME_ALT); if (StringUtils.isBlank(repoName)) return null; - var sourceBranchRaw = EnvHelper.env(ENV_SOURCE_BRANCH); + var sourceBranchRaw = env(ENV_SOURCE_BRANCH, ENV_SOURCE_BRANCH_ALT); var isPr = StringUtils.isNotBlank(sourceBranchRaw) && sourceBranchRaw.startsWith("refs/pull/"); var branchInfo = detectBranchInfo(isPr, sourceBranchRaw); var sourceBranch = branchInfo[0]; var targetBranch = branchInfo[1]; - var sha = EnvHelper.env(ENV_SOURCE_VERSION); - var repositoryId = EnvHelper.env(ENV_REPOSITORY_ID); - var buildId = EnvHelper.env(ENV_BUILD_ID); + var sha = env(ENV_SOURCE_VERSION, ENV_SOURCE_VERSION_ALT); + var repositoryId = env(ENV_REPOSITORY_ID, ENV_REPOSITORY_ID_ALT); + var buildId = env(ENV_BUILD_ID, ENV_BUILD_ID_ALT); // Build standardized structures // Extract simple repo name from full path if present @@ -104,7 +121,9 @@ public static AdoEnvironment detect() { var ciRepository = CiRepository.builder() .workspaceDir(EnvHelper.envOrDefault(ENV_SOURCES_DIRECTORY, - EnvHelper.envOrDefault(ENV_DEFAULT_WORKING_DIRECTORY, "."))) + () -> EnvHelper.envOrDefault(ENV_SOURCES_DIRECTORY_ALT, + () -> EnvHelper.envOrDefault(ENV_DEFAULT_WORKING_DIRECTORY, + () -> EnvHelper.envOrDefault(ENV_DEFAULT_WORKING_DIRECTORY_ALT, "."))))) .remoteUrl(null) // Not readily available in environment .name(CiRepositoryName.builder() .short_(shortRepoName) @@ -132,12 +151,12 @@ public static AdoEnvironment detect() { .build(); var pullRequest = isPr - ? CiPullRequest.active(EnvHelper.env(ENV_PR_ID), targetBranch) + ? CiPullRequest.active(env(ENV_PR_ID, ENV_PR_ID_ALT), targetBranch) : CiPullRequest.inactive(); return AdoEnvironment.builder() - .organization(EnvHelper.env(ENV_ORGANIZATION_URL)) - .project(EnvHelper.env(ENV_PROJECT)) + .organization(env(ENV_ORGANIZATION_URL, ENV_ORGANIZATION_URL_ALT)) + .project(env(ENV_PROJECT, ENV_PROJECT_ALT)) .repositoryId(repositoryId) .buildId(buildId) .ciRepository(ciRepository) @@ -145,6 +164,7 @@ public static AdoEnvironment detect() { .ciCommit(ciCommit) .pullRequest(pullRequest) .prTerminology(PR_TERMINOLOGY) + .prCommentActionSuffix(PR_COMMENT_ACTION_SUFFIX) .ciName(NAME) .ciId(ID) .build(); @@ -160,20 +180,29 @@ private static String[] detectBranchInfo(boolean isPr, String sourceBranchRaw) { if (isPr) { sourceBranch = EnvHelper.envOrDefault(ENV_PR_SOURCE_BRANCH, - EnvHelper.env(ENV_PR_SOURCE_BRANCH_NAME)); + () -> EnvHelper.envOrDefault(ENV_PR_SOURCE_BRANCH_ALT, + () -> EnvHelper.envOrDefault(ENV_PR_SOURCE_BRANCH_NAME, + EnvHelper.env(ENV_PR_SOURCE_BRANCH_NAME_ALT)))); sourceBranch = StringUtils.isNotBlank(sourceBranch) ? sourceBranch.replaceAll("^refs/heads/", "") : null; targetBranch = EnvHelper.envOrDefault(ENV_PR_TARGET_BRANCH, - EnvHelper.env(ENV_PR_TARGET_BRANCH_NAME)); + () -> EnvHelper.envOrDefault(ENV_PR_TARGET_BRANCH_ALT, + () -> EnvHelper.envOrDefault(ENV_PR_TARGET_BRANCH_NAME, + EnvHelper.env(ENV_PR_TARGET_BRANCH_NAME_ALT)))); targetBranch = StringUtils.isNotBlank(targetBranch) ? targetBranch.replaceAll("^refs/heads/", "") : null; } else { sourceBranch = EnvHelper.envOrDefault(ENV_SOURCE_BRANCH_NAME, - StringUtils.isNotBlank(sourceBranchRaw) ? sourceBranchRaw.replaceAll("^refs/heads/", "") : null); + () -> EnvHelper.envOrDefault(ENV_SOURCE_BRANCH_NAME_ALT, + StringUtils.isNotBlank(sourceBranchRaw) ? sourceBranchRaw.replaceAll("^refs/heads/", "") : null)); targetBranch = null; } return new String[]{sourceBranch, targetBranch}; } + + private static String env(String primaryName, String alternateName) { + return EnvHelper.envOrDefault(primaryName, () -> EnvHelper.env(alternateName)); + } /** * Get qualified repository name for Fortify (repo:branch format). diff --git a/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/bitbucket/BitbucketEnvironment.java b/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/bitbucket/BitbucketEnvironment.java index 003b65a6001..76a5fe12435 100644 --- a/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/bitbucket/BitbucketEnvironment.java +++ b/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/bitbucket/BitbucketEnvironment.java @@ -44,6 +44,7 @@ public record BitbucketEnvironment( String repositoryFullName, String pipelineUuid, String prTerminology, + String prCommentActionSuffix, String ciName, String ciId ) { @@ -51,6 +52,7 @@ public record BitbucketEnvironment( public static final String NAME = "Bitbucket"; public static final String ID = "bitbucket"; public static final String PR_TERMINOLOGY = "Pull Request"; + public static final String PR_COMMENT_ACTION_SUFFIX = "pr-comment"; public static final String ENV_WORKSPACE = "BITBUCKET_WORKSPACE"; public static final String ENV_REPO_OWNER = "BITBUCKET_REPO_OWNER"; @@ -138,6 +140,7 @@ public static BitbucketEnvironment detect() { .repositoryFullName(repoFullName) .pipelineUuid(EnvHelper.env(ENV_PIPELINE_UUID)) .prTerminology(PR_TERMINOLOGY) + .prCommentActionSuffix(PR_COMMENT_ACTION_SUFFIX) .ciName(NAME) .ciId(ID) .build(); diff --git a/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/github/GitHubEnvironment.java b/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/github/GitHubEnvironment.java index f12f0ac5477..eedf948c96e 100644 --- a/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/github/GitHubEnvironment.java +++ b/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/github/GitHubEnvironment.java @@ -52,6 +52,7 @@ public record GitHubEnvironment( // GitHub-specific properties String jobSummaryFile, String prTerminology, + String prCommentActionSuffix, String ciName, String ciId @@ -63,6 +64,7 @@ public record GitHubEnvironment( public static final String NAME = "GitHub"; public static final String ID = "github"; public static final String PR_TERMINOLOGY = "Pull Request"; + public static final String PR_COMMENT_ACTION_SUFFIX = "pr-comment"; // Environment variable names public static final String ENV_REPOSITORY = "GITHUB_REPOSITORY"; @@ -139,6 +141,7 @@ public static GitHubEnvironment detect() { .ciCommit(ciCommit) .pullRequest(pullRequest) .prTerminology(PR_TERMINOLOGY) + .prCommentActionSuffix(PR_COMMENT_ACTION_SUFFIX) .ciName(NAME) .ciId(ID) .build(); diff --git a/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/gitlab/GitLabEnvironment.java b/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/gitlab/GitLabEnvironment.java index c9f2bdc4e3e..384db0334da 100644 --- a/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/gitlab/GitLabEnvironment.java +++ b/fcli-core/fcli-common-ci/src/main/java/com/fortify/cli/common/ci/gitlab/GitLabEnvironment.java @@ -47,6 +47,7 @@ public record GitLabEnvironment( String projectId, String pipelineId, String prTerminology, + String prCommentActionSuffix, String ciName, String ciId ) { @@ -55,6 +56,7 @@ public record GitLabEnvironment( public static final String NAME = "GitLab"; public static final String ID = "gitlab"; public static final String PR_TERMINOLOGY = "Merge Request"; + public static final String PR_COMMENT_ACTION_SUFFIX = "mr-comment"; // Environment variable names public static final String ENV_GITLAB_CI = "GITLAB_CI"; @@ -141,6 +143,7 @@ public static GitLabEnvironment detect() { .ciCommit(ciCommit) .pullRequest(pullRequest) .prTerminology(PR_TERMINOLOGY) + .prCommentActionSuffix(PR_COMMENT_ACTION_SUFFIX) .ciName(NAME) .ciId(ID) .build(); diff --git a/fcli-core/fcli-common-ci/src/test/java/com/fortify/cli/common/ci/ado/AdoEnvironmentTest.java b/fcli-core/fcli-common-ci/src/test/java/com/fortify/cli/common/ci/ado/AdoEnvironmentTest.java index e95d11f55bb..cabd315780a 100644 --- a/fcli-core/fcli-common-ci/src/test/java/com/fortify/cli/common/ci/ado/AdoEnvironmentTest.java +++ b/fcli-core/fcli-common-ci/src/test/java/com/fortify/cli/common/ci/ado/AdoEnvironmentTest.java @@ -129,6 +129,33 @@ void testDetectPullRequestAlternativeEnvVars() { assertEquals("bugfix-abc", env.ciBranch().short_()); assertEquals("release", env.pullRequest().target()); } + + @Test + void testDetectPullRequestUppercaseEnvVars() { + System.setProperty("fcli.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", "https://dev.azure.com/myorg/"); + System.setProperty("fcli.env.SYSTEM_TEAMPROJECT", "MyProject"); + System.setProperty("fcli.env.BUILD_REPOSITORY_NAME", "MyRepo"); + System.setProperty("fcli.env.BUILD_REPOSITORY_ID", "ffffffff-1111-2222-3333-aaaaaaaaaaaa"); + System.setProperty("fcli.env.BUILD_BUILDID", "404"); + System.setProperty("fcli.env.BUILD_SOURCEBRANCH", "refs/pull/999/merge"); + System.setProperty("fcli.env.BUILD_SOURCEVERSION", "feedbeef1234"); + System.setProperty("fcli.env.SYSTEM_PULLREQUEST_SOURCEBRANCHNAME", "feature-uppercase"); + System.setProperty("fcli.env.SYSTEM_PULLREQUEST_TARGETBRANCHNAME", "main"); + System.setProperty("fcli.env.SYSTEM_PULLREQUEST_PULLREQUESTID", "999"); + System.setProperty("fcli.env.BUILD_SOURCESDIRECTORY", "/home/vsts/work/1/s"); + + var env = AdoEnvironment.detect(); + + assertNotNull(env); + assertEquals("MyProject", env.project()); + assertEquals("ffffffff-1111-2222-3333-aaaaaaaaaaaa", env.repositoryId()); + assertEquals("404", env.buildId()); + assertEquals("feature-uppercase", env.ciBranch().short_()); + assertEquals(true, env.pullRequest().active()); + assertEquals("999", env.pullRequest().id()); + assertEquals("main", env.pullRequest().target()); + assertEquals("/home/vsts/work/1/s", env.ciRepository().workspaceDir()); + } @Test void testGetQualifiedRepoName() { diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/ado-pr-comment.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/ado-pr-comment.yaml new file mode 100644 index 00000000000..1ed2ae81069 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/ado-pr-comment.yaml @@ -0,0 +1,163 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev-2.x.json + +# For now, this template uses latest release state to generate PR comments. + +author: Fortify +usage: + header: (PREVIEW) Add Azure DevOps Pull Request comment. + description: | + This action adds a comment to an Azure DevOps Pull Request. Currently + this is marked as PREVIEW as we build out this functionality; later + versions may have different behavior and/or require different action + cli. + + For best results, this fcli action should only be run on Azure DevOps + pull request build triggers. Upon PR creation, a new FoD release should + be created, copying state from the FoD release that represents the + branch into which the PR will be merged, and a new scan should be + run on the current PR branch before invoking this fcli action. + + This will ensure that scan results for the current PR will be + compared against the latest scan results for the target branch + upon PR creation. Optionally, new scans can be run upon PR changes, + creating new PR comments that show the issue delta compared to the + previous scan for this PR. + + Authentication defaults to "Authorization: Bearer " using + ADO_TOKEN/SYSTEM_ACCESSTOKEN or --ado-token, which is suitable for + Azure DevOps pipeline access tokens. If you want to use a PAT with + Basic auth, provide the full Authorization header through + ADO_AUTHORIZATION or --ado-authorization (for example, + "Basic "). The organization URL, project, repository ID, + and pull request ID can be automatically populated from Azure DevOps CI + environment variables. + +config: + rest.target.default: fod + +cli.options: + release: + names: --release, --rel + description: "Required release id or :[:]. Default value FOD_RELEASE environment variable." + required: true + default: ${#env('FOD_RELEASE')} + scan-type: + names: --scan-type, -t + description: "Scan type for which to list vulnerabilities. Default value: Static" + required: true + default: Static + ado-organization-url: + names: --ado-organization-url + description: "Azure DevOps organization URL. Auto-detected from ADO CI environment; provide for local testing. Default: System.TeamFoundationCollectionUri or ADO_ORGANIZATION_URL." + required: true + default: "${#env('System.TeamFoundationCollectionUri')!=null?#env('System.TeamFoundationCollectionUri'):#env('ADO_ORGANIZATION_URL')}" + ado-token: + names: --ado-token + description: "Azure DevOps token used with Bearer auth by default. Auto-detected from ADO CI environment; provide for local testing. Default: ADO_TOKEN or SYSTEM_ACCESSTOKEN." + required: false + default: "${#isNotBlank(#env('ADO_TOKEN'))?#env('ADO_TOKEN'):#env('SYSTEM_ACCESSTOKEN')}" + ado-authorization: + names: --ado-authorization + description: "Optional full Authorization header value (for example, Basic ). Default: ADO_AUTHORIZATION environment variable. If not set, this action uses Bearer with --ado-token." + required: false + default: ${#env('ADO_AUTHORIZATION')} + project: + names: --project + description: "Azure DevOps project name. Auto-detected from ADO CI environment; provide for local testing. Default: System.TeamProject or ADO_PROJECT." + required: false + default: "${#env('System.TeamProject')!=null?#env('System.TeamProject'):#env('ADO_PROJECT')}" + repository-id: + names: --repository-id + description: "Repository ID or name. Auto-detected from ADO CI environment; provide for local testing. Default: Build.Repository.ID or ADO_REPOSITORY_ID." + required: true + default: "${#env('Build.Repository.ID')!=null?#env('Build.Repository.ID'):#env('ADO_REPOSITORY_ID')}" + pr-id: + names: --pr-id + description: "Pull request ID. Auto-detected from ADO CI environment; provide for local testing. Default: System.PullRequest.PullRequestId or ADO_PR_ID." + required: true + default: "${#env('System.PullRequest.PullRequestId')!=null?#env('System.PullRequest.PullRequestId'):#env('ADO_PR_ID')}" + dryrun: + names: --dryrun + description: "Set to true to output request body without posting comment." + type: boolean + required: false + default: false + +steps: + - var.set: + adoEnv: ${#ado.env} + + - rest.target: + ado: + baseUrl: "${adoEnv!=null?adoEnv.organization:cli['ado-organization-url']}" + headers: + Authorization: "${#ifBlank(cli['ado-authorization'],'Bearer '+cli['ado-token'])}" + 'Content-Type': 'application/json' + - var.set: + rel: ${#fod.release(cli.release)} + - log.progress: Processing issue data + - rest.call: + issues: + uri: /api/v3/releases/${rel.releaseId}/vulnerabilities?limit=50 + query: + includeFixed: true + filters: scantype:${cli['scan-type']} + log.progress: + page.post-process: Processed ${totalIssueCount?:0} of ${issues_raw.totalCount} issues + records.for-each: + record.var-name: issue + if: ${issue.status!='Existing'} + do: + - var.set: + removedIssues..: {fmt: mdIssueListItem, if: "${issue.status=='Fix Validated'}"} + newIssues..: {fmt: mdIssueListItem, if: "${(issue.status=='New' || issue.status=='Reopen')}"} + + - var.set: + commentBody: {fmt: commentBody} + threadRequestBody: {fmt: threadRequestBody} + hasIssueStatusChanges: ${newIssues!=null || removedIssues!=null} + adoProjectPath: ${(adoEnv!=null?adoEnv.project:cli['project']).replace(' ','%20')} + + - if: ${cli.dryrun && hasIssueStatusChanges} + log.info: {msg: "${commentBody}"} + + - if: ${cli.dryrun && !hasIssueStatusChanges} + log.info: {msg: "No issue status changes detected; skipping PR thread."} + + - if: ${!cli.dryrun && hasIssueStatusChanges} + rest.call: + postPRThread: + method: POST + uri: "/${adoProjectPath}/_apis/git/repositories/${adoEnv!=null?adoEnv.repositoryId:cli['repository-id']}/pullRequests/${adoEnv!=null?adoEnv.pullRequest.id:cli['pr-id']}/threads" + target: ado + query: + 'api-version': '7.0' + body: ${threadRequestBody} + + - if: ${!cli.dryrun && !hasIssueStatusChanges} + log.info: {msg: "No issue status changes detected; not posting PR comment."} + +formatters: + threadRequestBody: + comments: + - content: ${commentBody} + + commentBody: | + ## Fortify vulnerability summary + + Any issues listed below are based on comparing the latest scan results against the previous + scan results in FoD release [${rel.applicationName}${#isNotBlank(rel.microserviceName)?' - '+rel.microserviceName:''} - ${rel.releaseName}](${#fod.releaseBrowserUrl(rel)}). + + ### New Issues + + ${newIssues==null + ? "* No new or re-introduced issues were detected" + : ("* "+#join('\n* ',newIssues))} + + ### Removed Issues + + ${removedIssues==null + ? "* No removed issues were detected" + : ("* "+#join('\n* ',removedIssues))} + + mdIssueListItem: "${issue.status} (${issue.scantype}) - ${issue.category}: \n[${issue.primaryLocationFull}${issue.lineNumber==null?'':':'+issue.lineNumber}](${#fod.issueBrowserUrl(issue)})" diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-mr-comment.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-mr-comment.yaml new file mode 100644 index 00000000000..9b079e37a4e --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-mr-comment.yaml @@ -0,0 +1,145 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev-2.x.json + +# For now, this template uses latest release state to generate MR comments. + +author: Fortify +usage: + header: (PREVIEW) Add GitLab Merge Request comment. + description: | + This action adds review comments to a GitLab Merge Request. Currently + this is marked as PREVIEW as we build out this functionality; later + versions may have different behavior and/or require different action + cli. + + For best results, this fcli action should only be run on GitLab + merge_request_event. Upon MR creation, a new FoD release should + be created, copying state from the FoD release that represents the + branch into which the MR will be merged, and a new scan should be + run on the current MR branch before invoking this fcli action. + + This will ensure that scan results for the current MR will be + compared against the latest scan results for the target branch + upon MR creation. Optionally, new scans can be run upon MR changes, + creating new MR comments that show the issue delta compared to the + previous scan for this MR. + + You will need to provide a GitLab token or set one of the environment + variables "GITLAB_TOKEN" (recommended) or "GITLAB_API_TOKEN" with + permissions to post comments on the MR, and the GitLab API v4 base URL + (e.g., https://gitlab.com/api/v4) if not using the default gitlab.com. + The project id and MR IID must also be provided, but can be automatically + populated from CI environment variables if running in GitLab CI. + +config: + rest.target.default: fod + +cli.options: + release: + names: --release, --rel + description: "Required release id or :[:]. Default value FOD_RELEASE environment variable." + required: true + default: ${#env('FOD_RELEASE')} + scan-type: + names: --scan-type, -t + description: "Scan type for which to list vulnerabilities. Default value Static." + required: true + default: Static + gitlab-api-url: + names: --gitlab-api-url + description: "Required GitLab API v4 base URL (e.g., https://gitlab.com/api/v4 or https://gitlab.example.com/api/v4). Default value CI_API_V4_URL environment variable." + required: true + default: ${#env('CI_API_V4_URL')} + gitlab-token: + names: --gitlab-token + description: "Required GitLab token. Defaults to GITLAB_TOKEN, falls back to GITLAB_API_TOKEN, then CI_JOB_TOKEN." + required: true + default: "${#ifBlank(#env('GITLAB_TOKEN'), #ifBlank(#env('GITLAB_API_TOKEN'), #env('CI_JOB_TOKEN')))}" + mask: + sensitivity: high + project-id: + names: --project-id + description: "Required GitLab project id. Default value CI_PROJECT_ID environment variable." + required: true + default: ${#env('CI_PROJECT_ID')} + mr-iid: + names: --mr-iid + description: "Required Merge Request IID. Default value CI_MERGE_REQUEST_IID environment variable." + required: true + default: ${#env('CI_MERGE_REQUEST_IID')} + dryrun: + names: --dryrun + description: "Set to true to output request body without posting comment." + type: boolean + required: false + default: false + +steps: + - rest.target: + gitlab: + baseUrl: ${cli['gitlab-api-url']} + headers: + PRIVATE-TOKEN: ${cli['gitlab-token']} + - var.set: + rel: ${#fod.release(cli.release)} + - log.progress: Processing issue data + - rest.call: + issues: + uri: /api/v3/releases/${rel.releaseId}/vulnerabilities?limit=50 + query: + includeFixed: true + filters: scantype:${cli['scan-type']} + log.progress: + page.post-process: Processed ${totalIssueCount?:0} of ${issues_raw.totalCount} issues + records.for-each: + record.var-name: issue + if: ${issue.status!='Existing'} + do: + - var.set: + removedIssues..: {fmt: mdIssueListItem, if: "${issue.status=='Fix Validated'}"} + newIssues..: {fmt: mdIssueListItem, if: "${(issue.status=='New' || issue.status=='Reopen')}"} + + - var.set: + reviewBody: {fmt: reviewBody} + noteRequestBody: {fmt: noteRequestBody} + hasIssueStatusChanges: ${newIssues!=null || removedIssues!=null} + + - if: ${cli.dryrun && hasIssueStatusChanges} + log.info: {msg: "${noteRequestBody}"} + + - if: ${cli.dryrun && !hasIssueStatusChanges} + log.info: {msg: "No issue status changes detected; skipping MR note."} + + - if: ${!cli.dryrun && hasIssueStatusChanges} + rest.call: + postMRNote: + method: POST + uri: /projects/${cli['project-id']}/merge_requests/${cli['mr-iid']}/notes + target: gitlab + body: ${noteRequestBody} + + - if: ${!cli.dryrun && !hasIssueStatusChanges} + log.info: {msg: "No issue status changes detected; not posting MR comment."} + +formatters: + noteRequestBody: + body: ${reviewBody} + + reviewBody: | + ## Fortify vulnerability summary + + Any issues listed below are based on comparing the latest scan results against the previous + scan results in FoD release [${rel.applicationName}${#isNotBlank(rel.microserviceName)?' - '+rel.microserviceName:''} - ${rel.releaseName}](${#fod.releaseBrowserUrl(rel)}). + + ### New Issues + + ${newIssues==null + ? "* No new or re-introduced issues were detected" + : ("* "+#join('\n* ',newIssues))} + + ### Removed Issues + + ${removedIssues==null + ? "* No removed issues were detected" + : ("* "+#join('\n* ',removedIssues))} + + mdIssueListItem: "${issue.status} (${issue.scantype}) - ${issue.category}: \n[${issue.primaryLocationFull}${issue.lineNumber==null?'':':'+issue.lineNumber}](${#fod.issueBrowserUrl(issue)})" diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/core/DetectEnvSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/core/DetectEnvSpec.groovy index 8da4460fddd..49bc1d2f760 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/core/DetectEnvSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/core/DetectEnvSpec.groovy @@ -36,6 +36,7 @@ class DetectEnvSpec extends FcliBaseSpec { // Verify non-PR properties it.any { it.contains("prActive: false") } it.any { it.contains("prNotActiveSkipReason: Not a Pull Request") } + it.any { it.contains("fod_prCommentAction: github-pr-comment") } } } @@ -64,6 +65,7 @@ class DetectEnvSpec extends FcliBaseSpec { // Verify non-PR properties (uses "Merge Request" terminology for GitLab) it.any { it.contains("prActive: false") } it.any { it.contains("prNotActiveSkipReason: Not a Merge Request") } + it.any { it.contains("fod_prCommentAction: gitlab-mr-comment") } } } @@ -150,6 +152,7 @@ class DetectEnvSpec extends FcliBaseSpec { it.any { it.contains("prId: 123") } it.any { it.contains("prTarget: main") } it.any { it.contains("prTerminology: Pull Request") } + it.any { it.contains("fod_prCommentAction: github-pr-comment") } } } @@ -177,6 +180,7 @@ class DetectEnvSpec extends FcliBaseSpec { it.any { it.contains("prId: 42") } it.any { it.contains("prTarget: main") } it.any { it.contains("prTerminology: Merge Request") } + it.any { it.contains("fod_prCommentAction: gitlab-mr-comment") } } }