rastray 0.15.0

Blazing-fast static analysis CLI for security, dependency, and performance audits.
param(
    [string]$Target = '',
    [string]$Tool = ''
)

$ErrorActionPreference = 'Stop'
$repoRoot = (Resolve-Path "$PSScriptRoot\..\..").Path
$workdir  = Join-Path $repoRoot 'bench-workdir'
$targetsRoot = Join-Path $workdir 'targets'
$resultsRoot = Join-Path $workdir 'results'
$rastray  = Join-Path $repoRoot 'target\release\rastray.exe'

$catalogue = @(
    [pscustomobject]@{ id='juice-shop';        path='juice-shop';        languages=@('javascript','typescript') }
    [pscustomobject]@{ id='nodegoat';          path='nodegoat';          languages=@('javascript','typescript') }
    [pscustomobject]@{ id='dvwa';              path='dvwa';              languages=@('php') }
    [pscustomobject]@{ id='railsgoat';         path='railsgoat';         languages=@('ruby') }
    [pscustomobject]@{ id='webgoat';           path='webgoat';           languages=@('java') }
    [pscustomobject]@{ id='django-defectdojo'; path='django-defectdojo'; languages=@('python') }
)

$toolList = @('rastray','bandit','semgrep','gosec','gitleaks','eslint-security')

function Is-Applicable {
    param([string]$ToolId, [object]$Target)
    switch ($ToolId) {
        'rastray'         { return $true }
        'semgrep'         { return $true }
        'gitleaks'        { return $true }
        'bandit'          { return $Target.languages -contains 'python' }
        'gosec'           { return $Target.languages -contains 'go' }
        'eslint-security' { return ($Target.languages -contains 'javascript') -or ($Target.languages -contains 'typescript') }
    }
    return $false
}

function Run-Rastray {
    param([string]$TargetDir, [string]$OutDir)
    $jsonPath = Join-Path $OutDir 'rastray.json'
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $jsonText = & $rastray $TargetDir --format json --offline 2>$null
    $sw.Stop()
    $jsonText | Set-Content -Path $jsonPath -Encoding utf8
    $count = 0
    try { $count = (($jsonText | ConvertFrom-Json).findings | Measure-Object).Count } catch {}
    return [pscustomobject]@{ tool='rastray'; version='0.8.0'; findings=$count; elapsed_ms=$sw.ElapsedMilliseconds }
}

function Run-Bandit {
    param([string]$TargetDir, [string]$OutDir)
    $jsonPath = Join-Path $OutDir 'bandit.json'
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $null = & bandit -q -r $TargetDir -f json -o $jsonPath 2>$null
    $sw.Stop()
    $count = 0
    if (Test-Path $jsonPath) {
        try { $count = ((Get-Content $jsonPath -Raw | ConvertFrom-Json).results | Measure-Object).Count } catch {}
    }
    return [pscustomobject]@{ tool='bandit'; version='1.9.4'; findings=$count; elapsed_ms=$sw.ElapsedMilliseconds }
}

function Run-Semgrep {
    param([string]$TargetDir, [string]$OutDir)
    $jsonPath = Join-Path $OutDir 'semgrep.json'
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $jsonText = & docker run --rm -v "${TargetDir}:/src" returntocorp/semgrep:latest semgrep scan --config=p/owasp-top-ten --json --quiet --metrics=off /src 2>$null
    $sw.Stop()
    $jsonText | Set-Content -Path $jsonPath -Encoding utf8
    $count = 0
    try { $count = (($jsonText | ConvertFrom-Json).results | Measure-Object).Count } catch {}
    return [pscustomobject]@{ tool='semgrep'; version='1.165.0'; findings=$count; elapsed_ms=$sw.ElapsedMilliseconds }
}

function Run-Gosec {
    param([string]$TargetDir, [string]$OutDir)
    $jsonPath = Join-Path $OutDir 'gosec.json'
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $null = & docker run --rm -v "${TargetDir}:/src" -w /src securego/gosec:latest -fmt=json -out=/src/.gosec-out.json -quiet ./... 2>$null
    $sw.Stop()
    $src = Join-Path $TargetDir '.gosec-out.json'
    if (Test-Path $src) { Move-Item -Force $src $jsonPath }
    $count = 0
    if (Test-Path $jsonPath) {
        try { $count = ((Get-Content $jsonPath -Raw | ConvertFrom-Json).Issues | Measure-Object).Count } catch {}
    }
    return [pscustomobject]@{ tool='gosec'; version='dev-2026-02-28'; findings=$count; elapsed_ms=$sw.ElapsedMilliseconds }
}

function Run-Gitleaks {
    param([string]$TargetDir, [string]$OutDir)
    $jsonPath = Join-Path $OutDir 'gitleaks.json'
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $prev = $ErrorActionPreference
    $ErrorActionPreference = 'Continue'
    try {
        & docker run --rm -v "${TargetDir}:/src" zricethezav/gitleaks:latest dir /src --report-path /src/.gitleaks-out.json --report-format json --exit-code 0 --no-banner 2>&1 | Out-Null
    } finally {
        $ErrorActionPreference = $prev
    }
    $sw.Stop()
    $src = Join-Path $TargetDir '.gitleaks-out.json'
    if (Test-Path $src) { Move-Item -Force $src $jsonPath }
    $count = 0
    if (Test-Path $jsonPath) {
        try {
            $parsed = Get-Content $jsonPath -Raw | ConvertFrom-Json
            if ($parsed -is [System.Array]) { $count = $parsed.Count } elseif ($parsed) { $count = 1 }
        } catch {}
    }
    return [pscustomobject]@{ tool='gitleaks'; version='v8.30.1'; findings=$count; elapsed_ms=$sw.ElapsedMilliseconds }
}

function Run-EslintSecurity {
    param([string]$TargetDir, [string]$OutDir)
    $jsonPath = Join-Path $OutDir 'eslint-security.json'
    $configDir = Join-Path $OutDir 'eslint-bench'
    New-Item -ItemType Directory -Force -Path $configDir | Out-Null
    $cfg = "import security from 'eslint-plugin-security';`nexport default [{`n  files: ['**/*.js','**/*.jsx','**/*.mjs','**/*.cjs','**/*.ts','**/*.tsx'],`n  plugins: { security },`n  rules: security.configs.recommended.rules`n}];`n"
    $cfg | Set-Content -Path (Join-Path $configDir 'eslint.config.mjs') -Encoding utf8
    Push-Location $configDir
    try {
        if (-not (Test-Path 'node_modules')) {
            $prev = $ErrorActionPreference
            $ErrorActionPreference = 'Continue'
            try {
                & npm init -y 2>&1 | Out-Null
                & npm install --silent --no-audit --no-fund eslint eslint-plugin-security 2>&1 | Out-Null
            } finally {
                $ErrorActionPreference = $prev
            }
        }
    } finally {
        Pop-Location
    }
    $targetCfg = Join-Path $TargetDir 'eslint.config.mjs'
    $targetNm  = Join-Path $TargetDir 'node_modules'
    $linkedNm  = $false
    $copiedCfg = $false
    if (-not (Test-Path $targetCfg)) {
        Copy-Item (Join-Path $configDir 'eslint.config.mjs') $targetCfg
        $copiedCfg = $true
    }
    if (-not (Test-Path $targetNm)) {
        cmd /c "mklink /J `"$targetNm`" `"$(Join-Path $configDir 'node_modules')`"" | Out-Null
        $linkedNm = $true
    }
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $prev = $ErrorActionPreference
    $ErrorActionPreference = 'Continue'
    try {
        Push-Location $TargetDir
        try {
            $jsonText = & npx --no-install eslint . -f json 2>$null
        } finally {
            Pop-Location
        }
    } finally {
        $ErrorActionPreference = $prev
    }
    $sw.Stop()
    if ($copiedCfg) { Remove-Item $targetCfg -Force -ErrorAction SilentlyContinue }
    if ($linkedNm)  { cmd /c "rmdir `"$targetNm`"" | Out-Null }
    $jsonText | Set-Content -Path $jsonPath -Encoding utf8
    $count = 0
    try {
        $arr = $jsonText | ConvertFrom-Json
        foreach ($file in $arr) { $count += $file.messages.Count }
    } catch {}
    return [pscustomobject]@{ tool='eslint-security'; version='node18+plugin-security3'; findings=$count; elapsed_ms=$sw.ElapsedMilliseconds }
}

function Invoke-Tool {
    param([string]$ToolId, [string]$TargetDir, [string]$OutDir)
    switch ($ToolId) {
        'rastray'         { return Run-Rastray         -TargetDir $TargetDir -OutDir $OutDir }
        'bandit'          { return Run-Bandit          -TargetDir $TargetDir -OutDir $OutDir }
        'semgrep'         { return Run-Semgrep         -TargetDir $TargetDir -OutDir $OutDir }
        'gosec'           { return Run-Gosec           -TargetDir $TargetDir -OutDir $OutDir }
        'gitleaks'        { return Run-Gitleaks        -TargetDir $TargetDir -OutDir $OutDir }
        'eslint-security' { return Run-EslintSecurity  -TargetDir $TargetDir -OutDir $OutDir }
    }
}

$activeTargets = if ($Target) { $catalogue | Where-Object { $_.id -eq $Target } } else { $catalogue }
$activeTools   = if ($Tool)   { $toolList  | Where-Object { $_ -eq $Tool } }       else { $toolList }

if (-not (Test-Path $resultsRoot)) { New-Item -ItemType Directory -Force -Path $resultsRoot | Out-Null }

$summaryPath = Join-Path $resultsRoot 'summary.json'
$summary = @()
if (Test-Path $summaryPath) {
    try {
        $existing = Get-Content $summaryPath -Raw | ConvertFrom-Json
        if ($existing) { $summary = @($existing) }
    } catch {}
}
foreach ($t in $activeTargets) {
    $targetDir = Join-Path $targetsRoot $t.path
    if (-not (Test-Path $targetDir)) { Write-Host "skip $($t.id): not cloned"; continue }
    $outDir = Join-Path $resultsRoot $t.id
    New-Item -ItemType Directory -Force -Path $outDir | Out-Null

    foreach ($toolId in $activeTools) {
        if (-not (Is-Applicable -ToolId $toolId -Target $t)) {
            Write-Host ("{0,-18} {1,-16} N/A" -f $t.id, $toolId)
            continue
        }
        Write-Host ("{0,-18} {1,-16} running..." -f $t.id, $toolId) -NoNewline
        try {
            $r = Invoke-Tool -ToolId $toolId -TargetDir $targetDir -OutDir $outDir
            Write-Host (" {0,5} findings  {1,7:N0} ms" -f $r.findings, $r.elapsed_ms)
            $summary = @($summary | Where-Object { -not ($_.target -eq $t.id -and $_.tool -eq $toolId) })
            $summary += [pscustomobject]@{
                target     = $t.id
                tool       = $toolId
                version    = $r.version
                findings   = $r.findings
                elapsed_ms = $r.elapsed_ms
            }
        } catch {
            Write-Host " ERROR: $_"
            $summary = @($summary | Where-Object { -not ($_.target -eq $t.id -and $_.tool -eq $toolId) })
            $summary += [pscustomobject]@{
                target     = $t.id
                tool       = $toolId
                version    = ''
                findings   = $null
                elapsed_ms = $null
                error      = $_.Exception.Message
            }
        }
    }
}

$summaryPath = Join-Path $resultsRoot 'summary.json'
$summary | ConvertTo-Json -Depth 4 | Set-Content -Path $summaryPath -Encoding utf8
Write-Host "`nsummary written to $summaryPath"
$summary | Format-Table -AutoSize target,tool,findings,elapsed_ms