Skip to main content
Glama
security-client.ts15.3 kB
/** * @fileoverview Security client for the DeepSource API * This module provides functionality for working with DeepSource security features. */ import { BaseDeepSourceClient } from './base-client.js'; import { VulnerabilityOccurrence, VulnerabilitySeverity, Vulnerability, } from '../models/security.js'; import { PaginatedResponse, PaginationParams } from '../utils/pagination/types.js'; import { isErrorWithMessage } from '../utils/errors/handlers.js'; import { ReportType } from '../deepsource.js'; /** * Compliance report data structure * @public */ export interface ComplianceReport { reportType: ReportType; status: 'PASSING' | 'FAILING' | 'NOOP'; title: string; description: string; severityDistribution: { critical: number; major: number; minor: number; total: number; }; trend?: { label?: string; value?: number; changePercentage?: number; }; categories: Array<{ name: string; status: 'PASSING' | 'FAILING' | 'NOOP'; issueCount: number; description?: string; }>; complianceScore: number; lastUpdated: string; } /** * 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: string, params: PaginationParams = {} ): Promise<PaginatedResponse<VulnerabilityOccurrence>> { try { this.logger.info('Fetching dependency vulnerabilities from DeepSource API', { projectKey, }); const project = await this.findProjectByKey(projectKey); if (!project) { return BaseDeepSourceClient.createEmptyPaginatedResponse<VulnerabilityOccurrence>(); } 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: string, reportType: ReportType ): Promise<ComplianceReport | null> { 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 */ private static buildVulnerabilitiesQuery(): string { 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 */ private static buildComplianceReportQuery(): string { 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 */ private extractVulnerabilitiesFromResponse(responseData: unknown): VulnerabilityOccurrence[] { const vulnerabilities: VulnerabilityOccurrence[] = []; try { const repository = (responseData as Record<string, unknown>)?.repository as Record< string, unknown >; const depVulns = repository?.dependencyVulnerabilities as Record<string, unknown>; const vulnEdges = (depVulns?.edges ?? []) as Array<Record<string, unknown>>; for (const edge of vulnEdges) { const node = edge.node as Record<string, unknown>; 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 */ private static isValidVulnerabilityNode(node: unknown): boolean { if (!node || typeof node !== 'object') { return false; } const record = node as Record<string, unknown>; return Boolean(record.id && record.package && record.packageVersion && record.vulnerability); } /** * Maps a vulnerability node to VulnerabilityOccurrence * @private */ private static mapVulnerabilityOccurrence( node: Record<string, unknown> ): VulnerabilityOccurrence { const packageInfo = (node.package as Record<string, unknown>) || {}; const packageVersion = (node.packageVersion as Record<string, unknown>) || {}; const vulnerability = (node.vulnerability as Record<string, unknown>) || {}; 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: Record<string, unknown> = { id: String(vulnerability.id ?? ''), identifier: String(vulnerability.identifier ?? ''), aliases: Array.isArray(vulnerability.aliases) ? (vulnerability.aliases as string[]) : [], 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') as VulnerabilitySeverity, introducedVersions: Array.isArray(vulnerability.introducedVersions) ? (vulnerability.introducedVersions as string[]) : [], fixedVersions: Array.isArray(vulnerability.fixedVersions) ? (vulnerability.fixedVersions as string[]) : [], referenceUrls: Array.isArray(vulnerability.referenceUrls) ? (vulnerability.referenceUrls as string[]) : [], }; // Return vulnerability object if (vulnerability.cvssV3BaseScore) { vuln.cvssV3BaseScore = Number(vulnerability.cvssV3BaseScore); } if (vulnerability.cvssV2BaseScore) { vuln.cvssV2BaseScore = Number(vulnerability.cvssV2BaseScore); } return vuln as unknown as Vulnerability; })(), reachability: 'UNKNOWN' as const, fixability: 'UNFIXABLE' as const, }; } /** * Extracts compliance report from GraphQL response * @private */ private extractComplianceReportFromResponse( responseData: unknown, reportType: ReportType, fieldName: string ): ComplianceReport | null { try { const repository = (responseData as Record<string, unknown>)?.repository as Record< string, unknown >; const reports = repository?.reports as Record<string, unknown>; const reportData = reports?.[fieldName] as Record<string, unknown>; if (!reportData) { return null; } const categories = (reportData.categories ?? []) as Array<Record<string, unknown>>; const status = String(reportData.status ?? 'NOOP') as 'PASSING' | 'FAILING' | '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') as 'PASSING' | 'FAILING' | '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 */ private static getReportFieldName(reportType: ReportType): string { 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 */ private static getReportTitle(reportType: ReportType): string { 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 */ private handleVulnerabilitiesError(error: unknown): PaginatedResponse<VulnerabilityOccurrence> { 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; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sapientpants/deepsource-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server