Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/cli/commands/_common/install/sca-scanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* SonarQube CLI
* Copyright (C) SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

// sca-scanner install: thin wrapper over the generic binary install pipeline.
// Version + signatures are placeholders until the binary is published on
// binaries.sonarsource.com — `installScaScannerBinary()` will throw until then,
// and `analyze dependency-risks` falls back to a mocked empty result.

import { SONAR_SCA_SCANNER_DIST_PREFIX } from '../../../../lib/config-constants';
import { SCA_SCANNER_BINARY_NAME } from '../../../../lib/install-types';
import {
SONAR_SCA_SCANNER_SIGNATURES,
SONAR_SCA_SCANNER_VERSION,
SONARSOURCE_PUBLIC_KEY,
} from '../../../../lib/signatures';
import { success } from '../../../../ui';
import { type BinarySpec, installBinary, resolveBinaryPath } from './binary';

const SCA_SCANNER_SPEC: BinarySpec = {
name: SCA_SCANNER_BINARY_NAME,
version: SONAR_SCA_SCANNER_VERSION,
distPrefix: SONAR_SCA_SCANNER_DIST_PREFIX,
signatures: SONAR_SCA_SCANNER_SIGNATURES,
publicKey: SONARSOURCE_PUBLIC_KEY,
};

export async function installScaScannerBinary(): Promise<string> {
const { binaryPath, freshlyInstalled } = await installBinary(SCA_SCANNER_SPEC);
if (freshlyInstalled) {
success(`sca-scanner installed at ${binaryPath}`);
}
return binaryPath;
}

export function resolveScaScannerBinaryPath(): string | null {
return resolveBinaryPath(SCA_SCANNER_SPEC);
}

export interface ScaScannerInstallerLike {
install(): Promise<string>;
}

export class DefaultScaScannerInstaller implements ScaScannerInstallerLike {
install(): Promise<string> {
return installScaScannerBinary();
}
}

// TODO(SCA wiring): remove this temporary hardcoded path and switch back to

Check warning on line 66 in src/cli/commands/_common/install/sca-scanner.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=SonarSource_sonarqube-cli&issues=AZ3kEV3RajwNQOLCXdIw&open=AZ3kEV3RajwNQOLCXdIw&pullRequest=240
// `DefaultScaScannerInstaller` once the binary is published.
const TEMP_SCA_SCANNER_PATH =
'C:\\Users\\georgii.borovinskikh\\Desktop\\tmp\\SCA\\sca-scanner-windows-x86-64.exe';

Check warning on line 69 in src/cli/commands/_common/install/sca-scanner.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=SonarSource_sonarqube-cli&issues=AZ3kEV3RajwNQOLCXdIx&open=AZ3kEV3RajwNQOLCXdIx&pullRequest=240

export class TempScaScannerInstaller implements ScaScannerInstallerLike {
install(): Promise<string> {
return Promise.resolve(TEMP_SCA_SCANNER_PATH);
}
}

export class MockScaScannerInstaller implements ScaScannerInstallerLike {
install(): Promise<string> {
return Promise.resolve('any path');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* SonarQube CLI
* Copyright (C) SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

// Routes the subset of `/api/settings/values` results consumed by SCA
// analysis. CLI-352 will forward these to sca-scanner as
// `--scanner-property`, `--excluded-path`, and `--include-git-ignored-paths`.

import { getValueAsList, type SettingsValue } from '../../../../sonarqube/settings-value.ts';

const SCA_KEY_PREFIX = 'sonar.sca.';
const EXCLUSION_KEYS = new Set([
'sonar.exclusions',
'sonar.global.exclusions',
'sonar.test.exclusions',
]);
const SCM_EXCLUSIONS_DISABLED_KEY = 'sonar.scm.exclusions.disabled';

export interface ScaProjectAnalysisProperties {
scaProperties: Record<string, string>;
exclusions: string[];
includeGitIgnoredPaths: boolean;
}

export function parseAnalysisProperties(settings: SettingsValue[]): ScaProjectAnalysisProperties {
const scaProperties: Record<string, string> = {};
const exclusions: string[] = [];
let includeGitIgnoredPaths = false;

for (const setting of settings) {
if (EXCLUSION_KEYS.has(setting.key)) {
exclusions.push(...getValueAsList(setting));
} else if (setting.key === SCM_EXCLUSIONS_DISABLED_KEY) {
includeGitIgnoredPaths = setting.value === 'true';
} else if (setting.key.startsWith(SCA_KEY_PREFIX)) {
const joined = setting.values?.join(',') ?? setting.value;
if (joined !== undefined) {
scaProperties[setting.key] = joined;
}
}
}

return { scaProperties, exclusions, includeGitIgnoredPaths };
}
108 changes: 108 additions & 0 deletions src/cli/commands/analyze/dependency-risk-helpers/dependency-risk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* SonarQube CLI
* Copyright (C) SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import type {
AnalyzeProjectIssue,
AnalyzeProjectRelease,
AnalyzeProjectResponse,
ScaIssueType,
SoftwareQuality,
} from './sca-scanner.ts';

export type DependencyRiskType = ScaIssueType;
export type DependencyRiskSeverity = 'INFO' | 'LOW' | 'MEDIUM' | 'HIGH' | 'BLOCKER';
export type { SoftwareQuality } from './sca-scanner.ts';
export type DependencyRiskStatus = 'OPEN' | 'CONFIRM' | 'ACCEPT' | 'SAFE' | 'FIXED';

export interface DependencyRisk {
packageName: string;
releaseKey: string;
issueKey?: string;
type: DependencyRiskType;
severity: DependencyRiskSeverity;
quality: SoftwareQuality;
status: DependencyRiskStatus;
newlyIntroduced: boolean;
dependencyFilePaths: string[];
dependencyChains: string[][];
licenseExpression?: string;
vulnerabilityId?: string;
cvssScore?: string;
cweIds?: string[];
}

const SEVERITIES: readonly DependencyRiskSeverity[] = ['INFO', 'LOW', 'MEDIUM', 'HIGH', 'BLOCKER'];

const SEVERITY_RANK: Record<DependencyRiskSeverity, number> = {
BLOCKER: 0,
HIGH: 1,
MEDIUM: 2,
LOW: 3,
INFO: 4,
};

export function sortDependencyRisks(risks: DependencyRisk[]): DependencyRisk[] {
return [...risks].sort((a, b) => {
const pkg = a.packageName.localeCompare(b.packageName);
if (pkg !== 0) return pkg;
const sev = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
if (sev !== 0) return sev;
return a.type.localeCompare(b.type);
});
}

export function toDependencyRisks(response: AnalyzeProjectResponse): DependencyRisk[] {
return response.releases.flatMap((release) =>
release.issues.map((issue) => toDependencyRisk(release, issue)),
);
}

function toDependencyRisk(
release: AnalyzeProjectRelease,
issue: AnalyzeProjectIssue,
): DependencyRisk {
const risk: DependencyRisk = {
packageName: `${release.packageName}@${release.version}`,
releaseKey: release.key,
issueKey: issue.key,
type: issue.type,
severity: normalizeSeverity(issue.severity),
quality: issue.quality,
status: (issue.status as DependencyRiskStatus | undefined) ?? 'OPEN',
newlyIntroduced: release.newlyIntroduced,
dependencyFilePaths: release.dependencyFilePaths,
dependencyChains: release.dependencyChains,
};
if (issue.type === 'VULNERABILITY') {
risk.vulnerabilityId = issue.vulnerabilityId;
risk.cvssScore = issue.cvssScore;
risk.cweIds = issue.cweIds;
} else if (issue.type === 'PROHIBITED_LICENSE') {
risk.licenseExpression = issue.spdxLicenseId ?? release.licenseExpression ?? undefined;
}
return risk;
}

function normalizeSeverity(raw: string): DependencyRiskSeverity {
const upper = raw.toUpperCase();
return (SEVERITIES as readonly string[]).includes(upper)
? (upper as DependencyRiskSeverity)
: 'INFO';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* SonarQube CLI
* Copyright (C) SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import type { DependencyRisk, DependencyRiskType } from './dependency-risk.ts';
import type { AnalysisErrorResource } from './sca-scanner.ts';

const TYPE_LABELS: Record<DependencyRiskType, string> = {
VULNERABILITY: 'Vulnerability',
PROHIBITED_LICENSE: 'License',
MALWARE: 'Malware',
};

export function formatDependencyRisksTable(
risks: DependencyRisk[],
packagesScanned: number,
errors: AnalysisErrorResource[],
): string {
const lines: string[] = [
`Scan Summary: ${packagesScanned} dependencies checked. ${risks.length} risks found`,
];

if (risks.length === 0) {
appendErrors(lines, errors);
return lines.join('\n');
}

lines.push('');

const rows = risks.map((risk) => ({
severity: risk.severity,
status: risk.status,
type: TYPE_LABELS[risk.type],
pkg: risk.newlyIntroduced ? `${risk.packageName} [NEW]` : risk.packageName,
manifest: risk.dependencyFilePaths.join(', '),
issue: issueCell(risk),
remediation: remediationCell(risk),
}));

const widths = {
severity: columnWidth('SEVERITY', rows, (r) => r.severity),
status: columnWidth('STATUS', rows, (r) => r.status),
type: columnWidth('TYPE', rows, (r) => r.type),
pkg: columnWidth('PACKAGE', rows, (r) => r.pkg),
manifest: columnWidth('MANIFEST', rows, (r) => r.manifest),
issue: columnWidth('ISSUE', rows, (r) => r.issue),
};

const header = [
'SEVERITY'.padEnd(widths.severity),
'STATUS'.padEnd(widths.status),
'TYPE'.padEnd(widths.type),
'PACKAGE'.padEnd(widths.pkg),
'MANIFEST'.padEnd(widths.manifest),
'ISSUE'.padEnd(widths.issue),
'REMEDIATION',
].join(' ');

const separator = '-'.repeat(header.length);

lines.push(header, separator);
for (const row of rows) {
lines.push(
[
row.severity.padEnd(widths.severity),
row.status.padEnd(widths.status),
row.type.padEnd(widths.type),
row.pkg.padEnd(widths.pkg),
row.manifest.padEnd(widths.manifest),
row.issue.padEnd(widths.issue),
row.remediation,
]
.join(' ')
.trimEnd(),
);
}
lines.push(separator);
appendErrors(lines, errors);

return lines.join('\n');
}

function appendErrors(lines: string[], errors: AnalysisErrorResource[]): void {
if (errors.length === 0) {
return;
}
lines.push('', 'Errors:');
for (const err of errors) {
const prefix = ` [${err.code}]`;
lines.push(err.path ? `${prefix} ${err.path}: ${err.message}` : `${prefix} ${err.message}`);
}
}

function issueCell(risk: DependencyRisk): string {
switch (risk.type) {
case 'VULNERABILITY':
return risk.vulnerabilityId ?? '';
case 'PROHIBITED_LICENSE':
return risk.licenseExpression ?? '';
case 'MALWARE':
return 'Malicious package';
}
}

function remediationCell(risk: DependencyRisk): string {
switch (risk.type) {
case 'MALWARE':
return 'Remove dependency';
case 'PROHIBITED_LICENSE':
case 'VULNERABILITY':
return '';
}
}

function columnWidth<T>(header: string, rows: T[], pick: (r: T) => string): number {
return Math.max(header.length, ...rows.map((r) => pick(r).length));
}
Loading
Loading