deepsource-mcp-server
Version:
Model Context Protocol server for DeepSource
413 lines • 15 kB
JavaScript
/**
* @fileoverview Security client for the DeepSource API
* This module provides functionality for working with DeepSource security features.
*/
import { BaseDeepSourceClient } from './base-client.js';
import { isErrorWithMessage } from '../utils/errors/handlers.js';
import { ReportType } from '../deepsource.js';
/**
* Client for interacting with DeepSource security API
* @class
* @extends BaseDeepSourceClient
* @public
*/
export class SecurityClient extends BaseDeepSourceClient {
/**
* Fetches dependency vulnerabilities from a DeepSource project
* @param projectKey The project key to fetch vulnerabilities for
* @param params Optional pagination parameters
* @returns Promise that resolves to a paginated list of vulnerability occurrences
* @throws {ClassifiedError} When the API request fails
* @public
*/
async getDependencyVulnerabilities(projectKey, params = {}) {
try {
this.logger.info('Fetching dependency vulnerabilities from DeepSource API', {
projectKey,
});
const project = await this.findProjectByKey(projectKey);
if (!project) {
return BaseDeepSourceClient.createEmptyPaginatedResponse();
}
const normalizedParams = BaseDeepSourceClient.normalizePaginationParams(params);
const query = SecurityClient.buildVulnerabilitiesQuery();
const response = await this.executeGraphQL(query, {
login: project.repository.login,
name: project.repository.name,
provider: project.repository.provider,
...normalizedParams,
});
if (!response.data) {
throw new Error('No data received from GraphQL API');
}
const vulnerabilities = this.extractVulnerabilitiesFromResponse(response.data);
this.logger.info('Successfully fetched dependency vulnerabilities', {
count: vulnerabilities.length,
});
return {
items: vulnerabilities,
pageInfo: {
hasNextPage: false, // Simplified for now
hasPreviousPage: false,
},
totalCount: vulnerabilities.length,
};
}
catch (error) {
return this.handleVulnerabilitiesError(error);
}
}
/**
* Fetches a compliance report for a specific report type
* @param projectKey The project key
* @param reportType The type of compliance report
* @returns Promise that resolves to compliance report if available, null otherwise
* @public
*/
async getComplianceReport(projectKey, reportType) {
try {
this.logger.info('Fetching compliance report from DeepSource API', {
projectKey,
reportType,
});
// Validate report type
if (reportType !== ReportType.OWASP_TOP_10 &&
reportType !== ReportType.SANS_TOP_25 &&
reportType !== ReportType.MISRA_C) {
throw new Error(`Unsupported report type: ${reportType}`);
}
const project = await this.findProjectByKey(projectKey);
if (!project) {
return null;
}
const query = SecurityClient.buildComplianceReportQuery();
const fieldName = SecurityClient.getReportFieldName(reportType);
const response = await this.executeGraphQL(query, {
login: project.repository.login,
name: project.repository.name,
provider: project.repository.provider,
});
if (!response.data) {
return null;
}
const reportData = this.extractComplianceReportFromResponse(response.data, reportType, fieldName);
if (reportData) {
this.logger.info('Successfully fetched compliance report', {
reportType,
status: reportData.status,
});
}
return reportData;
}
catch (error) {
if (isErrorWithMessage(error, 'NoneType') || isErrorWithMessage(error, 'not found')) {
return null;
}
throw error;
}
}
/**
* Builds GraphQL query for dependency vulnerabilities
* @private
*/
static buildVulnerabilitiesQuery() {
return `
query getDependencyVulnerabilities(
$login: String!
$name: String!
$provider: VCSProvider!
$first: Int
$after: String
) {
repository(login: $login, name: $name, vcsProvider: $provider) {
dependencyVulnerabilities(first: $first, after: $after) {
edges {
node {
id
package {
id
ecosystem
name
}
packageVersion {
id
version
}
vulnerability {
id
identifier
summary
details
severity
cvssV3BaseScore
cvssV2BaseScore
fixedVersions
aliases
referenceUrls
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
}
`;
}
/**
* Builds GraphQL query for compliance reports
* @private
*/
static buildComplianceReportQuery() {
return `
query getComplianceReports(
$login: String!
$name: String!
$provider: VCSProvider!
) {
repository(login: $login, name: $name, vcsProvider: $provider) {
name
id
reports {
owaspTop10 {
status
categories {
name
status
criticalCount: count(severity: CRITICAL)
majorCount: count(severity: MAJOR)
minorCount: count(severity: MINOR)
total: count
}
}
sansTop25 {
status
categories {
name
status
criticalCount: count(severity: CRITICAL)
majorCount: count(severity: MAJOR)
minorCount: count(severity: MINOR)
total: count
}
}
misraC {
status
categories {
name
status
criticalCount: count(severity: CRITICAL)
majorCount: count(severity: MAJOR)
minorCount: count(severity: MINOR)
total: count
}
}
}
}
}
`;
}
/**
* Extracts vulnerabilities from GraphQL response
* @private
*/
extractVulnerabilitiesFromResponse(responseData) {
const vulnerabilities = [];
try {
const repository = responseData?.repository;
const depVulns = repository?.dependencyVulnerabilities;
const vulnEdges = (depVulns?.edges ?? []);
for (const edge of vulnEdges) {
const node = edge.node;
if (SecurityClient.isValidVulnerabilityNode(node)) {
vulnerabilities.push(SecurityClient.mapVulnerabilityOccurrence(node));
}
}
}
catch (error) {
this.logger.error('Error extracting vulnerabilities from response', { error });
}
return vulnerabilities;
}
/**
* Validates if a vulnerability node has required structure
* @private
*/
static isValidVulnerabilityNode(node) {
if (!node || typeof node !== 'object') {
return false;
}
const record = node;
return Boolean(record.id && record.package && record.packageVersion && record.vulnerability);
}
/**
* Maps a vulnerability node to VulnerabilityOccurrence
* @private
*/
static mapVulnerabilityOccurrence(node) {
const packageInfo = node.package || {};
const packageVersion = node.packageVersion || {};
const vulnerability = node.vulnerability || {};
return {
id: String(node.id ?? ''),
package: {
id: String(packageInfo.id ?? ''),
ecosystem: String(packageInfo.ecosystem ?? ''),
name: String(packageInfo.name ?? ''),
},
packageVersion: {
id: String(packageVersion.id ?? ''),
version: String(packageVersion.version ?? ''),
},
vulnerability: (() => {
const vuln = {
id: String(vulnerability.id ?? ''),
identifier: String(vulnerability.identifier ?? ''),
aliases: Array.isArray(vulnerability.aliases) ? vulnerability.aliases : [],
summary: String(vulnerability.summary ?? ''),
details: String(vulnerability.details ?? ''),
publishedAt: String(vulnerability.publishedAt ?? new Date().toISOString()),
updatedAt: String(vulnerability.updatedAt ?? new Date().toISOString()),
severity: String(vulnerability.severity ?? 'NONE'),
introducedVersions: Array.isArray(vulnerability.introducedVersions)
? vulnerability.introducedVersions
: [],
fixedVersions: Array.isArray(vulnerability.fixedVersions)
? vulnerability.fixedVersions
: [],
referenceUrls: Array.isArray(vulnerability.referenceUrls)
? vulnerability.referenceUrls
: [],
}; // Return vulnerability object
if (vulnerability.cvssV3BaseScore) {
vuln.cvssV3BaseScore = Number(vulnerability.cvssV3BaseScore);
}
if (vulnerability.cvssV2BaseScore) {
vuln.cvssV2BaseScore = Number(vulnerability.cvssV2BaseScore);
}
return vuln;
})(),
reachability: 'UNKNOWN',
fixability: 'UNFIXABLE',
};
}
/**
* Extracts compliance report from GraphQL response
* @private
*/
extractComplianceReportFromResponse(responseData, reportType, fieldName) {
try {
const repository = responseData?.repository;
const reports = repository?.reports;
const reportData = reports?.[fieldName];
if (!reportData) {
return null;
}
const categories = (reportData.categories ?? []);
const status = String(reportData.status ?? 'NOOP');
// Calculate severity distribution
let totalCritical = 0;
let totalMajor = 0;
let totalMinor = 0;
let totalIssues = 0;
const processedCategories = categories.map((category) => {
const criticalCount = Number(category.criticalCount ?? 0);
const majorCount = Number(category.majorCount ?? 0);
const minorCount = Number(category.minorCount ?? 0);
const total = Number(category.total ?? 0);
totalCritical += criticalCount;
totalMajor += majorCount;
totalMinor += minorCount;
totalIssues += total;
return {
name: String(category.name ?? ''),
status: String(category.status ?? 'NOOP'),
issueCount: total,
description: `${criticalCount} critical, ${majorCount} major, ${minorCount} minor issues`,
};
});
// Calculate compliance score (0-100)
const complianceScore = totalIssues === 0
? 100
: Math.max(0, 100 - (totalCritical * 10 + totalMajor * 5 + totalMinor));
return {
reportType,
status,
title: SecurityClient.getReportTitle(reportType),
description: `${reportType.replace(/_/g, ' ')} compliance analysis`,
severityDistribution: {
critical: totalCritical,
major: totalMajor,
minor: totalMinor,
total: totalIssues,
},
categories: processedCategories,
complianceScore: Math.round(complianceScore),
lastUpdated: new Date().toISOString(),
};
}
catch (error) {
this.logger.error('Error extracting compliance report from response', { error });
return null;
}
}
/**
* Gets the GraphQL field name for a report type
* @private
*/
static getReportFieldName(reportType) {
switch (reportType) {
case ReportType.OWASP_TOP_10:
return 'owaspTop10';
case ReportType.SANS_TOP_25:
return 'sansTop25';
case ReportType.MISRA_C:
return 'misraC';
default:
throw new Error(`Unsupported report type: ${reportType}`);
}
}
/**
* Gets the human-readable title for a report type
* @private
*/
static getReportTitle(reportType) {
switch (reportType) {
case ReportType.OWASP_TOP_10:
return 'OWASP Top 10';
case ReportType.SANS_TOP_25:
return 'SANS Top 25';
case ReportType.MISRA_C:
return 'MISRA C';
default:
return reportType.replace(/_/g, ' ');
}
}
/**
* Handles errors during vulnerabilities fetching
* @private
*/
handleVulnerabilitiesError(error) {
this.logger.error('Error in getDependencyVulnerabilities', {
errorType: typeof error,
errorMessage: error instanceof Error ? error.message : String(error),
});
if (isErrorWithMessage(error, 'NoneType')) {
return {
items: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
totalCount: 0,
};
}
throw error;
}
}
//# sourceMappingURL=security-client.js.map