tasks/analysis.tasks.ps1

$covenantVersion = "0.19.0"
$CovenantIncludeSpdxReport = $true
$CovenantIncludeCycloneDxReport = $true
$CovenantMetadata = @{
    git_repo = $(if (Get-Command "gh" -ErrorAction SilentlyContinue) {
        try {
            $ghRepo = & gh repo view --json nameWithOwner
            $ghRepo | ConvertFrom-Json | Select-Object -expandproperty nameWithOwner
        }
        catch {
            ""
        }
    })
    git_branch = $(if (Get-Command "git" -ErrorAction SilentlyContinue) {git branch --show-current })
    git_sha = $(if (Get-Command "git" -ErrorAction SilentlyContinue) { & git rev-parse HEAD })
}

# Defaults for publishing-related variables
$PublishCovenantOutputToStorage = $false
$AnalysisOutputStorageAccountName = ""
$AnalysisOutputContainerName = ""
$AnalysisOutputBlobPath = ""

task InstallCovenantTool {
    Install-DotNetTool -Name covenant -Version $covenantVersion
}

# Synopsis: Setup SBOM metadata for Covenant command-line
task PrepareCovenantMetadata {

    $script:covenantMetadataArgs = @()
    foreach ($key in $CovenantMetadata.Keys) {
        # NOTE: No space after the '-m' switch otherwise the metadata key has a leading space in the report
        $script:covenantMetadataArgs += "-m$key=$($CovenantMetadata[$key])"
    }
}

task RunCovenantTool -If { $SolutionToBuild } Version,
                                              InstallCovenantTool,
                                              PrepareCovenantMetadata,{

    $baseOutputName = [IO.Path]::GetFileNameWithoutExtension($SolutionToBuild)
    # Ensure we have a fully-qualified path, as this will be needed when uploading on build server
    $script:covenantJsonOutputFile = Join-Path $here ("/{0}.sbom.json" -f $baseOutputName)
    $script:covenantSpdxOutputFile = Join-Path $here ("/{0}.sbom.spdx.json" -f $baseOutputName)
    $script:covenantCycloneDxOutputFile = Join-Path $here ("/{0}.sbom.cyclonedx.xml" -f $baseOutputName)
    $script:covenantHtmlReportFile = Join-Path $here ("/{0}.sbom.html" -f $baseOutputName)
    Write-Verbose "covenantHtmlReportFile: $covenantHtmlReportFile"

    # Generate SBOM
    exec {
        & dotnet-covenant `
                    generate `
                    $SolutionToBuild `
                    -v $script:GitVersion.SemVer `
                    --output $covenantJsonOutputFile `
                    $covenantMetadataArgs
    }

    # Generate HTML report
    exec {
        & dotnet-covenant `
                    report `
                    $covenantJsonOutputFile `
                    --output $covenantHtmlReportFile

    }
}

# Synopsis: Generate SPDX-formatted report
task GenerateCovenantSpdxReport -If { $SolutionToBuild -and $CovenantIncludeSpdxReport } RunCovenantTool,{
    exec {
        & dotnet-covenant `
                    convert `
                    spdx `
                    $covenantJsonOutputFile `
                    --output $covenantSpdxOutputFile

    }
    Write-Verbose "covenantSpdxOutputFile: $covenantSpdxOutputFile"
}

# Synopsis: Generate CycloneDX-formatted report
task GenerateCovenantCycloneDxReport -If { $SolutionToBuild -and $CovenantIncludeCycloneDxReport } RunCovenantTool,{
    exec {
        & dotnet-covenant `
                    convert `
                    cyclonedx `
                    $covenantJsonOutputFile `
                    --output $covenantCycloneDxOutputFile

    }
    Write-Verbose "covenantCycloneDxOutputFile: $covenantCycloneDxOutputFile"
}

task PublishCovenantBuildArtefacts -If { $IsAzureDevops } {
    Write-Host "##vso[task.setvariable variable=SbomHtmlReportPath;isoutput=true]$covenantHtmlReportFile"
    Write-Host "##vso[artifact.upload artifactname=SBOM]$covenantHtmlReportFile"
    Write-Host "##vso[artifact.upload artifactname=SBOM]$covenantJsonOutputFile"

    if ($CovenantIncludeSpdxReport) {
        Write-Host "##vso[artifact.upload artifactname=SBOM]$covenantSpdxOutputFile"
    }

    if ($CovenantIncludeCycloneDxReport) {
        Write-Host "##vso[artifact.upload artifactname=SBOM]$covenantCycloneDxOutputFile"
    }
}

task PublishCovenantOutputToStorage -If { $SolutionToBuild -and $PublishCovenantOutputToStorage } {
    if ( (Test-Path $covenantJsonOutputFile) -and `
            $AnalysisOutputStorageAccountName -and `
            $AnalysisOutputContainerName -and `
            $AnalysisOutputBlobPath) {
    
        $covenantJsonOutputFilename = (Split-Path -Leaf $covenantJsonOutputFile)
        $filename = "{0}-{1}.json" -f [IO.Path]::GetFileNameWithoutExtension($covenantJsonOutputFilename),
                                     ([DateTime]::Now).ToString('yyyyMMddHHmmssfff')

        Write-Information @"
Publishing storage account:
    Source File: $covenantJsonOutputFile
    Account: $AnalysisOutputStorageAccountName
    Blob Path: "$AnalysisOutputContainerName/$AnalysisOutputBlobPath/$filename"
"@


        $uri = "https://{0}.blob.core.windows.net/{1}/{2}/{3}" -f $AnalysisOutputStorageAccountName,
                        $AnalysisOutputContainerName,
                        $AnalysisOutputBlobPath,
                        $filename

        $authUri = & az storage blob generate-sas `
                                --auth-mode login `
                                --as-user `
                                --https-only `
                                --account-name $AnalysisOutputStorageAccountName `
                                --blob-url $uri `
                                --permissions c `
                                --start (Get-Date).ToUniversalTime().ToString("yyyy-M-d'T'H:m'Z'") `
                                --expiry (Get-Date).AddMinutes(10).ToUniversalTime().ToString("yyyy-M-d'T'H:m'Z'") `
                                --full-uri `
                                -o tsv
        if ($LASTEXITCODE -ne 0) {
            Write-Warning "Unable to generate a storage SAS token for publishing SBOM - have you run 'az login'?"
        }
        else {
            $headers = @{
                "x-ms-date" = [System.DateTime]::UtcNow.ToString("R")
                "x-ms-blob-type" = "BlockBlob"
            }
            Invoke-RestMethod -Headers $headers `
                      -Uri $authUri `
                      -Method PUT `
                      -InFile $covenantJsonOutputFile `
                      -Verbose:$false | Out-Null
    
            Write-Information "Covenant JSON output published to storage account"
        }
    }
    else {
        Write-Information "Publishing of Covenant output skipped, due to absent configuration"
    }
}

task RunSBOMAnalysis -If { $SolutionToBuild -and $env:SBOM_ANALYSIS_RELEASE_READER_PAT } EnsureGitHubCli,RunCovenantTool,{
    if (!(Get-Module Az.Storage -ListAvailable)){
        Install-Module Az.Storage -Scope CurrentUser -Repository PSGallery -Force -Verbose
    }
    
    $AnalysisOutputContainerName = "data"
    $AnalysisOutputStorageAccountName = "endsynapsedatalake"
    # 1. Download JSON ruleset
    $uri = "https://{0}.blob.core.windows.net/{1}/{2}/{3}" -f $AnalysisOutputStorageAccountName,
                        $AnalysisOutputContainerName,
                        "openchain/license_rules",
                        "license_rule_set.json"
    $isAuthenticated = $false
    try{
        $authUri = exec {& az storage blob generate-sas `
                                --auth-mode login `
                                --as-user `
                                --https-only `
                                --account-name $AnalysisOutputStorageAccountName `
                                --blob-url $uri `
                                --permissions re `
                                --start (Get-Date).ToUniversalTime().ToString("yyyy-M-d'T'H:m'Z'") `
                                --expiry (Get-Date).AddMinutes(10).ToUniversalTime().ToString("yyyy-M-d'T'H:m'Z'") `
                                --full-uri `
                                -o tsv}
        $isAuthenticated = $true
    }
    catch{
        Write-Warning "Skipping SBOM Analysis, unable to access the license rule set. Ensure you are logged into the Azure CLI"
    }
    if($isAuthenticated){
        Write-Host $authUri
        $analysisFilesLocation = '.analysis'
        if(!(Test-Path $analysisFilesLocation)){
            New-Item -ItemType Directory $analysisFilesLocation | Out-Null
        }
        Get-AzStorageBlobContent -Destination "$($analysisFilesLocation)/" -absoluteuri $authUri -Force | fl 

        # Switch to a PAT that gives read access to the repo hosting the analysis tool
        $savedGhToken = $env:GH_TOKEN
        $env:GH_TOKEN = $env:SBOM_ANALYSIS_RELEASE_READER_PAT
        try {
            # Find latest version released on GitHub
            $latestVersion = exec { gh release list -R endjin/endjin-sbom-analyser --limit 1 } |
                ConvertFrom-Csv -Header title,type,"tag name",published -Delimiter `t |
                Select-Object -ExpandProperty "tag name"
        
            if (!$latestVersion) {
                throw "Unable to determine the latest version of the Python tool"
            }
            Write-Host $latestVersion

            $DownloadFileName = "sbom_analyser-$($latestVersion)-py3-none-any.whl"
            if(!(Test-Path (Join-Path $analysisFilesLocation $DownloadFileName))){
                Write-Host "Downloading latest release of SBOM Analyser, version" $latestVersion
                exec { & gh release download -R "endjin/endjin-sbom-analyser" -p $DownloadFileName -D $analysisFilesLocation}
            }
        }
        finally {
            $env:GH_TOKEN = $savedGhToken
        }
        
        exec {
            pip install poetry
            pip install (Join-Path $analysisFilesLocation $DownloadFileName)
        }
        $sbomPath = $covenantJsonOutputFile
        Write-Host $sbomPath
        $jsonPath = Get-ChildItem -path "$($analysisFilesLocation)/openchain/license_rules/*.json"
        Write-Host $jsonPath
        
        Push-Location $analysisFilesLocation
        try{
            exec{
                generate_sbom_score $sbomPath $jsonPath
            }
            $summarisedContent = Get-Content 'sbom_analysis_summarised_scores.csv' | ConvertFrom-Csv

            if ($summarisedContent.Unknown -gt 0){ 
                Write-Warning (Write-SBOMComponents -fileName 'sbom_analysis_unknown_components.csv' -sum $summarisedContent.Unknown -type 'unknown')
            }
            if ($summarisedContent.Rejected -gt 0){
                throw (Write-SBOMComponents -fileName 'sbom_analysis_rejected_components.csv' -sum $summarisedContent.Rejected -type 'rejected')
            }
        }
        finally{
            Pop-Location
        }
    }
}

task RunCovenant RunCovenantTool,
                 GenerateCovenantSpdxReport,
                 GenerateCovenantCycloneDxReport,
                 PublishCovenantBuildArtefacts,
                 PublishCovenantOutputToStorage,
                 RunSBOMAnalysis