UNPKG

git-assure

Version:

A tool for analyzing GitHub repositories for sustainability and security risks.

967 lines (945 loc) 53.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.analyzeGitHubRepository = analyzeGitHubRepository; const semver_1 = __importDefault(require("semver")); /** * Function to Analyze a GitHub Repository * * @param {string} repoUrl - The URL of the GitHub repository to analyze. * @returns {Promise<AnalysisResult>} - A promise that resolves to an object containing the analysis results. */ async function analyzeGitHubRepository(repoUrl) { try { const parts = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/); if (!parts || parts.length !== 3) { return { markdownSummary: 'Error: Invalid GitHub repository URL.', riskScore: 'N/A' }; } const owner = parts[1]; const repo = parts[2].replace(/\.git$/, ''); // Remove potential .git extension const apiUrl = `https://api.github.com/repos/${owner}/${repo}`; // --- Configure Authentication Headers --- const headers = { Accept: 'application/vnd.github.v3+json' }; // Use GitHub token if available const githubToken = process.env.GH_TOKEN; if (githubToken) { headers['Authorization'] = `token ${githubToken}`; console.log('Using GitHub authentication token'); } else { console.log('No GitHub token found. Using unauthenticated requests (rate limits apply)'); } // --- Helper Function for Authenticated Requests --- const fetchWithAuth = (url) => fetch(url, { headers }); // --- Fetch Basic Repository Information --- const repoInfoResponse = await fetchWithAuth(apiUrl); if (!repoInfoResponse.ok) { return { markdownSummary: `Error fetching repository information (status ${repoInfoResponse.status}): ${repoInfoResponse.statusText}`, riskScore: 'N/A' }; } const repoInfo = (await repoInfoResponse.json()); const createdAt = new Date(repoInfo.created_at); const updatedAt = new Date(repoInfo.updated_at); const ageInDays = Math.floor((Date.now() - createdAt.getTime()) / (1000 * 60 * 60 * 24)); // --- Get Community Metrics --- const starCount = repoInfo.stargazers_count; const forkCount = repoInfo.forks_count; const watcherCount = repoInfo.subscribers_count; const openIssuesCount = repoInfo.open_issues_count; // --- Get License Information --- let license = 'Unknown'; let licenseRisk = 'Medium'; if (repoInfo.license && repoInfo.license.name) { license = repoInfo.license.name; // Assess license risk (permissive licenses are lower risk) const permissiveLicenses = ['MIT', 'Apache', 'BSD', 'ISC', 'CC0']; const restrictiveLicenses = ['GPL', 'AGPL', 'LGPL', 'MPL']; if (permissiveLicenses.some(l => license.includes(l))) { licenseRisk = 'Low'; } else if (restrictiveLicenses.some(l => license.includes(l))) { licenseRisk = 'Medium'; } else if (license.includes('Proprietary') || license === 'UNLICENSED') { licenseRisk = 'High'; } } // --- Fetch Contributors --- const contributorsResponse = await fetchWithAuth(`${apiUrl}/contributors`); const contributors = (await contributorsResponse.json()); const numberOfContributors = contributors.length; // --- Fetch Contributor Details --- let totalContributorAge = 0; let processedContributors = 0; // Process up to 10 top contributors to avoid API rate limits const contributorsToProcess = contributors.slice(0, 10); for (const contributor of contributorsToProcess) { try { const userResponse = await fetchWithAuth(contributor.url); if (userResponse.ok) { const userData = (await userResponse.json()); const userCreatedAt = new Date(userData.created_at); const userAgeInDays = Math.floor((Date.now() - new Date(userCreatedAt).getTime()) / (1000 * 60 * 60 * 24)); totalContributorAge += userAgeInDays; processedContributors++; } } catch (error) { console.warn(`Could not fetch details for contributor: ${contributor.login}`, error.message); } } const averageContributorAgeInDays = processedContributors > 0 ? Math.floor(totalContributorAge / processedContributors) : null; const averageContributorAgeInYears = averageContributorAgeInDays ? (averageContributorAgeInDays / 365).toFixed(1) : null; // --- Check for Dependencies Info --- const packageJsonResponse = await fetchWithAuth(`${apiUrl}/contents/package.json`); const packageLockResponse = await fetchWithAuth(`${apiUrl}/contents/package-lock.json`); const requirementsResponse = await fetchWithAuth(`${apiUrl}/contents/requirements.txt`); const gemfileResponse = await fetchWithAuth(`${apiUrl}/contents/Gemfile`); const gradleResponse = await fetchWithAuth(`${apiUrl}/contents/build.gradle`); const mavenResponse = await fetchWithAuth(`${apiUrl}/contents/pom.xml`); const hasDependencyFile = [ packageJsonResponse, packageLockResponse, requirementsResponse, gemfileResponse, gradleResponse, mavenResponse ].some(response => response.ok); // --- Analyze Dependencies for Vulnerabilities --- let dependencyAnalysis = { hasDependencies: hasDependencyFile, dependenciesCount: 0, parsedDependencies: [], outdatedDependencies: [], majorOutdatedCount: 0, minorOutdatedCount: 0, vulnerablePackages: [], alertsEnabled: false, highSeverityCount: null, mediumSeverityCount: null, lowSeverityCount: null, vulnerabilitySource: null }; // Check if vulnerability alerts are enabled (requires OAuth token with right permissions) if (githubToken) { try { const alertsResponse = await fetch(`${apiUrl}/vulnerability-alerts`, { method: 'GET', headers: { ...headers, Accept: 'application/vnd.github.dorian-preview+json' } }); dependencyAnalysis.alertsEnabled = alertsResponse.status === 204; // Try to get actual vulnerability alerts (requires permissions) if (dependencyAnalysis.alertsEnabled) { const dependabotAlertsResponse = await fetch(`${apiUrl}/dependabot/alerts?state=open`, { headers: { ...headers, Accept: 'application/vnd.github.dorian-preview+json' } }); if (dependabotAlertsResponse.ok) { const alerts = await dependabotAlertsResponse.json(); // Count by severity const severityCounts = { high: 0, medium: 0, low: 0 }; const vulnerablePackages = []; alerts.forEach(alert => { // Count by severity if (alert.security_vulnerability && alert.security_vulnerability.severity) { const severity = alert.security_vulnerability.severity.toLowerCase(); if (severity in severityCounts) { if (typeof severity === 'string' && severity in severityCounts) { severityCounts[severity]++; } } } // Track vulnerable packages if (alert.security_advisory && alert.security_advisory.vulnerabilities) { alert.security_advisory.vulnerabilities.forEach((vuln) => { if (vuln.package && vuln.package.name) { vulnerablePackages.push({ name: vuln.package.name, severity: vuln.severity || 'unknown', fixedIn: vuln.patched_versions || 'unknown' }); } }); } }); dependencyAnalysis.highSeverityCount = severityCounts.high; dependencyAnalysis.mediumSeverityCount = severityCounts.medium; dependencyAnalysis.lowSeverityCount = severityCounts.low; dependencyAnalysis.vulnerablePackages = vulnerablePackages; } } } catch (error) { console.warn('Could not check vulnerability alerts:', error.message); } } // --- Parse dependencies from package.json if available if (packageJsonResponse.ok) { try { const contentResponse = (await packageJsonResponse.json()); if (contentResponse.content) { const content = Buffer.from(contentResponse.content, 'base64').toString('utf8'); const packageJson = JSON.parse(content); // Combine all dependencies const allDeps = { ...(packageJson.dependencies || {}), ...(packageJson.devDependencies || {}) }; dependencyAnalysis.dependenciesCount = Object.keys(allDeps).length; // Extract dependency info dependencyAnalysis.parsedDependencies = Object.entries(allDeps) .map(([name, version]) => ({ name, version: version.replace(/[^0-9.]/g, '') // Clean up version string })) .slice(0, 20); // Limit to top 20 to avoid too much data } } catch (error) { console.warn('Could not parse package.json:', error.message); } } // Parse Python requirements if available if (requirementsResponse.ok && dependencyAnalysis.parsedDependencies.length === 0) { try { const contentResponse = (await requirementsResponse.json()); if (contentResponse.content) { const content = Buffer.from(contentResponse.content, 'base64').toString('utf8'); // Simple regex to extract package==version const requirements = content .split('\n') .map(line => { const match = line.match(/^([a-zA-Z0-9_.-]+)[=~!<>]{1,2}([0-9a-zA-Z.-]+)/); return match ? { name: match[1], version: match[2] } : null; }) .filter((item) => item !== null); dependencyAnalysis.dependenciesCount = requirements.length; dependencyAnalysis.parsedDependencies = requirements.slice(0, 20); // Limit to top 20 } } catch (error) { console.warn('Could not parse requirements.txt:', error.message); } } // --- Check for Vulnerability Information via OSV Database --- if (dependencyAnalysis.parsedDependencies.length > 0) { try { console.log('Checking OSV database for vulnerabilities...'); // Determine ecosystem based on detected files let ecosystem = null; if (packageJsonResponse.ok) { ecosystem = 'npm'; } else if (requirementsResponse.ok) { ecosystem = 'PyPI'; } else if (mavenResponse.ok) { ecosystem = 'Maven'; } if (ecosystem) { // Process dependencies in batches to avoid overloading the API const batchSize = 10; const batches = []; for (let i = 0; i < dependencyAnalysis.parsedDependencies.length; i += batchSize) { batches.push(dependencyAnalysis.parsedDependencies.slice(i, i + batchSize)); } let osvVulnerabilities = []; for (const batch of batches) { const vulnerabilityPromises = batch.map(async (dep) => { try { // Query OSV API for each dependency const osvApiUrl = 'https://api.osv.dev/v1/query'; const response = await fetch(osvApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ package: { name: dep.name, ecosystem: ecosystem }, version: dep.version }) }); if (response.ok) { const data = (await response.json()); if (data.vulns && data.vulns.length > 0) { // Process and return vulnerabilities return data.vulns.map((vuln) => ({ name: dep.name, version: dep.version, id: vuln.id, details: vuln.summary || 'No summary provided', severity: determineSeverity(vuln), fixedIn: extractFixedVersions(vuln), package: dep.name })); } } return []; } catch (err) { console.warn(`Error checking vulnerability for ${dep.name}@${dep.version}:`, err.message); return []; } }); // Wait for all vulnerability checks in this batch const batchResults = await Promise.all(vulnerabilityPromises); osvVulnerabilities = [...osvVulnerabilities, ...batchResults.flat()]; // Small delay to avoid rate limiting await new Promise(resolve => setTimeout(resolve, 500)); } // Count vulnerabilities by severity const highSeverityVulns = osvVulnerabilities.filter(v => v.severity === 'HIGH').length; const mediumSeverityVulns = osvVulnerabilities.filter(v => v.severity === 'MEDIUM').length; const lowSeverityVulns = osvVulnerabilities.filter(v => v.severity === 'LOW').length; // Only update counts if we found vulnerabilities via OSV and don't already have GHAS data if (osvVulnerabilities.length > 0 && dependencyAnalysis.highSeverityCount === 0 && dependencyAnalysis.mediumSeverityCount === 0 && dependencyAnalysis.lowSeverityCount === 0) { dependencyAnalysis.highSeverityCount = highSeverityVulns; dependencyAnalysis.mediumSeverityCount = mediumSeverityVulns; dependencyAnalysis.lowSeverityCount = lowSeverityVulns; dependencyAnalysis.vulnerablePackages = osvVulnerabilities; dependencyAnalysis.vulnerabilitySource = 'OSV'; } } } catch (error) { console.warn('Could not check OSV for vulnerabilities:', error.message); } } // --- Check for outdated dependencies --- if (dependencyAnalysis.parsedDependencies.length > 0) { try { console.log('Checking for outdated dependencies...'); // Determine package registry based on detected ecosystem let ecosystem = null; if (packageJsonResponse.ok) { ecosystem = 'npm'; } else if (requirementsResponse.ok) { ecosystem = 'PyPI'; } if (ecosystem) { // Process dependencies in batches to avoid overloading the API const batchSize = 5; const batches = []; for (let i = 0; i < dependencyAnalysis.parsedDependencies.length; i += batchSize) { batches.push(dependencyAnalysis.parsedDependencies.slice(i, i + batchSize)); } const outdatedDependencies = []; for (const batch of batches) { const versionPromises = batch.map(async (dep) => { try { let latestVersion = null; // Check npm registry for JavaScript packages if (ecosystem === 'npm') { const npmResponse = await fetch(`https://registry.npmjs.org/${encodeURIComponent(dep.name)}/latest`); if (npmResponse.ok) { const npmData = (await npmResponse.json()); latestVersion = npmData.version; } } // Check PyPI for Python packages else if (ecosystem === 'PyPI') { const pypiResponse = await fetch(`https://pypi.org/pypi/${encodeURIComponent(dep.name)}/json`); if (pypiResponse.ok) { const pypiData = (await pypiResponse.json()); latestVersion = pypiData.info.version; } } if (latestVersion && dep.version) { // Clean up version strings for proper comparison const cleanCurrentVersion = dep.version.replace(/[^0-9.]/g, ''); const cleanLatestVersion = latestVersion.replace(/[^0-9.]/g, ''); // Only process if we have valid versions if (semver_1.default.valid(semver_1.default.coerce(cleanCurrentVersion)) && semver_1.default.valid(semver_1.default.coerce(cleanLatestVersion))) { // Calculate version difference const currentSemver = semver_1.default.coerce(cleanCurrentVersion); const latestSemver = semver_1.default.coerce(cleanLatestVersion); if (currentSemver && latestSemver) { const isMajorBehind = semver_1.default.major(latestSemver) > semver_1.default.major(currentSemver); const isMinorBehind = semver_1.default.major(latestSemver) === semver_1.default.major(currentSemver) && semver_1.default.minor(latestSemver) > semver_1.default.minor(currentSemver); const isPatchBehind = semver_1.default.major(latestSemver) === semver_1.default.major(currentSemver) && semver_1.default.minor(latestSemver) === semver_1.default.minor(currentSemver) && semver_1.default.patch(latestSemver) > semver_1.default.patch(currentSemver); const versionsBehind = { major: semver_1.default.major(latestSemver) - semver_1.default.major(currentSemver), minor: isMinorBehind ? semver_1.default.minor(latestSemver) - semver_1.default.minor(currentSemver) : 0, patch: isPatchBehind ? semver_1.default.patch(latestSemver) - semver_1.default.patch(currentSemver) : 0 }; // Return outdated info if package is behind if (isMajorBehind || isMinorBehind || isPatchBehind) { return { name: dep.name, version: dep.version, currentVersion: cleanCurrentVersion, latestVersion: cleanLatestVersion, isMajorBehind, isMinorBehind, isPatchBehind, versionsBehind, updateUrgency: isMajorBehind ? 'high' : isMinorBehind ? 'medium' : 'low' }; } } } } return null; } catch (err) { console.warn(`Error checking latest version for ${dep.name}:`, err.message); return null; } }); // Wait for all version checks in this batch const batchResults = await Promise.all(versionPromises); const outdatedInBatch = batchResults.filter((item) => item !== null); outdatedDependencies.push(...outdatedInBatch); // Small delay to avoid rate limiting await new Promise(resolve => setTimeout(resolve, 500)); } dependencyAnalysis.outdatedDependencies = outdatedDependencies; // Count outdated dependencies by severity dependencyAnalysis.majorOutdatedCount = dependencyAnalysis.outdatedDependencies.filter(dep => dep.isMajorBehind).length; dependencyAnalysis.minorOutdatedCount = dependencyAnalysis.outdatedDependencies.filter(dep => !dep.isMajorBehind && dep.isMinorBehind).length; } } catch (error) { console.warn('Could not check for outdated dependencies:', error.message); } } // --- Check for Contributing Guidelines --- const contributingGuidelinesResponse = await fetchWithAuth(`${apiUrl}/contents/CONTRIBUTING.md`); const hasContributingGuidelines = contributingGuidelinesResponse.ok; // --- Check for Security Policy --- const securityPolicyResponse = await fetchWithAuth(`${apiUrl}/contents/SECURITY.md`); const hasSecurityPolicy = securityPolicyResponse.ok; // --- Check for Test Files --- const testDirResponses = await Promise.all([ fetchWithAuth(`${apiUrl}/contents/tests`), fetchWithAuth(`${apiUrl}/contents/test`), fetchWithAuth(`${apiUrl}/contents/__tests__`), fetchWithAuth(`${apiUrl}/contents/spec`) ]); const hasTestDirectory = testDirResponses.some(response => response.ok); // --- Check for CI/CD Setup --- const ciConfigResponses = await Promise.all([ fetchWithAuth(`${apiUrl}/contents/.github/workflows`), fetchWithAuth(`${apiUrl}/contents/.travis.yml`), fetchWithAuth(`${apiUrl}/contents/.gitlab-ci.yml`), fetchWithAuth(`${apiUrl}/contents/azure-pipelines.yml`), fetchWithAuth(`${apiUrl}/contents/Jenkinsfile`), fetchWithAuth(`${apiUrl}/contents/.circleci/config.yml`) ]); const hasCiSetup = ciConfigResponses.some(response => response.ok); // --- Check Documentation Quality --- const readmeResponse = await fetchWithAuth(`${apiUrl}/contents/README.md`); const hasReadme = readmeResponse.ok; // Get README excerpt if available let readmeExcerpt = null; let readmeUrl = null; if (hasReadme) { try { const readmeData = (await readmeResponse.json()); readmeUrl = readmeData.html_url; // GitHub URL to view the full README // Decode README content if not too large if (readmeData.content && readmeData.size && readmeData.size < 100000) { // avoid trying to process huge READMEs const fullReadme = Buffer.from(readmeData.content, 'base64').toString('utf8'); // Remove code blocks first const readmeWithoutCode = fullReadme .replace(/```[\s\S]*?```/g, '') // Remove fenced code blocks with ``` .replace(/~~~[\s\S]*?~~~/g, '') // Remove fenced code blocks with ~~~ .replace(/`[^`]+`/g, ''); // Remove inline code // Extract a meaningful excerpt (first few paragraphs, limited to ~500 chars) const paragraphs = readmeWithoutCode.split('\n\n'); let excerpt = ''; // Start with the first paragraph that's not just a heading or badges for (const paragraph of paragraphs) { const trimmed = paragraph.trim(); // Skip if paragraph meets any of these conditions if (trimmed.startsWith('#') || // Skip headings trimmed.length <= 20 || // Skip very short lines trimmed.match(/^[-*]/) || // Skip list items // Skip badge/shield lines (more precise detection) /!\[.*?\]\(.*?badge.*?\)/i.test(trimmed) || // Badge image links /!\[.*?\]\(.*?shield.*?\)/i.test(trimmed) || // Shield image links /\[!\[.*?\]\(.*?\)\]\(.*?\)/i.test(trimmed) || // Linked badge pattern // Skip lines with multiple image links (likely badge rows) (trimmed.match(/!\[/g)?.length && trimmed.match(/!\[/g).length > 1) || // Skip lines that are mostly image links and little text (trimmed.match(/!\[/g)?.length && trimmed.match(/!\[/g).length / trimmed.length > 0.1)) { continue; } excerpt = trimmed; break; } // If we didn't find a good first paragraph, use the first non-empty one if (!excerpt && paragraphs.length > 0) { for (const paragraph of paragraphs) { const trimmed = paragraph.trim(); if (trimmed.length > 20 && !trimmed.startsWith('#')) { excerpt = trimmed; break; } } } // Limit excerpt length and clean it up if (excerpt.length > 500) { excerpt = excerpt.substring(0, 500) + '...'; } // Clean up any remaining markdown artifacts excerpt = excerpt .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Replace [text](link) with just text .replace(/\*\*([^*]+)\*\*/g, '$1') // Remove bold formatting .replace(/\*([^*]+)\*/g, '$1') // Remove italic formatting .replace(/_{2,}/g, '') // Remove horizontal rules .replace(/\n+/g, ' ') // Replace multiple newlines with spaces .trim(); readmeExcerpt = excerpt; } } catch (error) { console.warn('Could not parse README:', error.message); } } const docsResponses = await Promise.all([ fetchWithAuth(`${apiUrl}/contents/docs`), fetchWithAuth(`${apiUrl}/contents/documentation`), fetchWithAuth(`${apiUrl}/contents/wiki`) ]); const hasDocDirectory = docsResponses.some(response => response.ok); // --- Check Code Quality Tools --- const codeQualityResponses = await Promise.all([ fetchWithAuth(`${apiUrl}/contents/.eslintrc`), fetchWithAuth(`${apiUrl}/contents/.eslintrc.js`), fetchWithAuth(`${apiUrl}/contents/.eslintrc.json`), fetchWithAuth(`${apiUrl}/contents/.prettierrc`), fetchWithAuth(`${apiUrl}/contents/.prettierrc.js`), fetchWithAuth(`${apiUrl}/contents/.prettierrc.json`), fetchWithAuth(`${apiUrl}/contents/.stylelintrc`), fetchWithAuth(`${apiUrl}/contents/.pylintrc`), fetchWithAuth(`${apiUrl}/contents/tslint.json`), fetchWithAuth(`${apiUrl}/contents/.rubocop.yml`) ]); const hasCodeQualityTools = codeQualityResponses.some(response => response.ok); // --- Check for Releases --- const releasesResponse = await fetchWithAuth(`${apiUrl}/releases`); let releaseInfo = { hasReleases: false, releaseCount: 0, latestReleaseDate: null, daysSinceLastRelease: null, usesSemanticVersioning: false }; if (releasesResponse.ok) { const releases = (await releasesResponse.json()); releaseInfo.hasReleases = releases.length > 0; releaseInfo.releaseCount = releases.length; if (releases.length > 0) { const latestRelease = releases[0]; releaseInfo.latestReleaseDate = new Date(latestRelease.published_at); releaseInfo.daysSinceLastRelease = Math.floor((Date.now() - releaseInfo.latestReleaseDate.getTime()) / (1000 * 60 * 60 * 24)); // Check if using semver (e.g., v1.0.0) const semverRegex = /^v?\d+\.\d+\.\d+(-.*)?$/; releaseInfo.usesSemanticVersioning = semverRegex.test(latestRelease.tag_name); } } // --- Fetch Commit Activity (rough estimate of frequency) --- const commitsResponse = await fetchWithAuth(`${apiUrl}/commits?per_page=100`); // Get last 100 commits const commits = (await commitsResponse.json()); const numberOfCommits = commits.length; const lastCommitDate = commits.length > 0 ? new Date(commits[0].commit.author.date) : null; const daysSinceLastCommit = lastCommitDate ? Math.floor((Date.now() - lastCommitDate.getTime()) / (1000 * 60 * 60 * 24)) : null; // --- Fetch Open Pull Requests --- const pullsResponse = await fetchWithAuth(`${apiUrl}/pulls?state=open`); const openPulls = (await pullsResponse.json()); const longLivingPulls = openPulls.filter(pull => { const createdDate = new Date(pull.created_at).getTime(); const ageInDays = Math.floor((Date.now() - createdDate) / (1000 * 60 * 60 * 24)); return ageInDays > 90; // Consider PRs open for more than 90 days as long-lived }); // --- Fetch Open Issues --- const issuesResponse = await fetchWithAuth(`${apiUrl}/issues?state=open&sort=created&direction=asc`); const openIssues = (await issuesResponse.json()); // Filter out pull requests from the issues list const actualOpenIssues = openIssues.filter(issue => !issue.pull_request); const longLivingIssues = actualOpenIssues.filter(issue => { const createdDate = new Date(issue.created_at).getTime(); const ageInDays = Math.floor((Date.now() - createdDate) / (1000 * 60 * 60 * 24)); return ageInDays > 180; // Consider issues open for more than 180 days as long-lived }); // --- Calculate Response Time Metrics --- let responseTimeMetrics = null; if (actualOpenIssues && actualOpenIssues.length > 0) { // Get a sample of recently closed issues to check response time const closedIssuesResponse = await fetchWithAuth(`${apiUrl}/issues?state=closed&sort=updated&direction=desc&per_page=30`); if (closedIssuesResponse.ok) { const closedIssues = (await closedIssuesResponse.json()); const issuesWithComments = []; // Get response times for a sample of issues for (const issue of closedIssues.slice(0, 10)) { if (!issue.pull_request) { // Exclude PRs from this analysis const commentsResponse = await fetchWithAuth(issue.comments_url); if (commentsResponse.ok) { const comments = (await commentsResponse.json()); if (comments.length > 0) { const createdDate = new Date(issue.created_at); const firstResponseDate = new Date(comments[0].created_at); const responseTimeHours = Math.floor((firstResponseDate.getTime() - createdDate.getTime()) / (1000 * 60 * 60)); issuesWithComments.push({ number: issue.number, responseTimeHours }); } } } } if (issuesWithComments.length > 0) { const totalResponseTime = issuesWithComments.reduce((sum, issue) => sum + issue.responseTimeHours, 0); responseTimeMetrics = { averageResponseHours: Math.floor(totalResponseTime / issuesWithComments.length), sampleSize: issuesWithComments.length }; } } } // --- Code Complexity (Very difficult to assess accurately via API) --- let codeComplexity = 'Difficult to assess via API.'; if (repoInfo.size) { if (repoInfo.size < 500) { codeComplexity = 'Likely low complexity (based on size).'; } else if (repoInfo.size < 5000) { codeComplexity = 'Potentially moderate complexity (based on size).'; } else { codeComplexity = 'Likely high complexity (based on size).'; } } // --- Calculate Risk Score --- let riskScore = 0; const riskFactors = []; // Sustainability Factors if (numberOfContributors < 2) { riskScore += 3; riskFactors.push('Low number of contributors.'); } else if (numberOfContributors < 5) { riskScore += 1; riskFactors.push('Relatively low number of contributors.'); } // Consider contributor maturity (GitHub account age) if (averageContributorAgeInDays !== null) { if (averageContributorAgeInDays < 180) { riskScore += 2; riskFactors.push('Contributors have relatively new GitHub accounts (< 6 months).'); } else if (averageContributorAgeInDays < 365) { riskScore += 1; riskFactors.push('Contributors have moderately new GitHub accounts (< 1 year).'); } } if (ageInDays < 365) { riskScore += 1; riskFactors.push('Relatively young project.'); } if (daysSinceLastCommit === null || daysSinceLastCommit > 90) { riskScore += 2; riskFactors.push('Infrequent recent contributions.'); } else if (daysSinceLastCommit > 30) { riskScore += 1; riskFactors.push('Potentially infrequent recent contributions.'); } // License risk factor if (license === 'Unknown') { riskScore += 3; riskFactors.push('No license found.'); } else if (licenseRisk === 'High') { riskScore += 2; riskFactors.push('Restrictive license may limit usage.'); } else if (licenseRisk === 'Medium') { riskScore += 1; riskFactors.push('License has some usage restrictions.'); } // Dependencies risk if (!hasDependencyFile) { riskScore += 1; riskFactors.push('No dependency management file found.'); } // Test coverage risk if (!hasTestDirectory) { riskScore += 2; riskFactors.push('No test directory found.'); } // CI/CD setup if (!hasCiSetup) { riskScore += 1; riskFactors.push('No CI/CD configuration found.'); } // Documentation risk if (!hasReadme) { riskScore += 2; riskFactors.push('No README file found.'); } if (!hasDocDirectory && !hasReadme) { riskScore += 1; riskFactors.push('Limited documentation.'); } // Release practices if (!releaseInfo.hasReleases) { riskScore += 1; riskFactors.push('No formal releases found.'); } else if (releaseInfo.daysSinceLastRelease !== null && releaseInfo.daysSinceLastRelease > 365) { riskScore += 2; riskFactors.push('No releases in over a year.'); } else if (!releaseInfo.usesSemanticVersioning) { riskScore += 1; riskFactors.push('Not using semantic versioning.'); } // Community metrics if (starCount < 10) { riskScore += 1; riskFactors.push('Low community interest (few stars).'); } // Response time metrics if (responseTimeMetrics && responseTimeMetrics.averageResponseHours > 168) { // More than 1 week riskScore += 2; riskFactors.push('Slow response time to issues (>1 week).'); } else if (responseTimeMetrics && responseTimeMetrics.averageResponseHours > 72) { // More than 3 days riskScore += 1; riskFactors.push('Moderate response time to issues (>3 days).'); } // Code quality tools if (!hasCodeQualityTools) { riskScore += 1; riskFactors.push('No code quality tools found.'); } // Existing factors: contributing guidelines, PRs, issues, security policy, code complexity if (!hasContributingGuidelines) { riskScore += 1; riskFactors.push('No contributing guidelines found.'); } if (longLivingPulls.length > 5) { riskScore += 2; riskFactors.push(`Many long-lived open pull requests (${longLivingPulls.length}).`); } else if (longLivingPulls.length > 0) { riskScore += 1; riskFactors.push(`Some long-lived open pull requests (${longLivingPulls.length}).`); } if (longLivingIssues.length > 10) { riskScore += 3; riskFactors.push(`Many long-lived open issues (${longLivingIssues.length}).`); } else if (longLivingIssues.length > 5) { riskScore += 2; riskFactors.push(`Several long-lived open issues (${longLivingIssues.length}).`); } else if (longLivingIssues.length > 0) { riskScore += 1; riskFactors.push(`Some long-lived open issues (${longLivingIssues.length}).`); } // Security Factors if (!hasSecurityPolicy) { riskScore += 2; riskFactors.push('No explicit security policy found.'); } if (codeComplexity.startsWith('Likely high')) { riskScore += 1; riskFactors.push('Potentially high code complexity.'); } // Add vulnerability scoring if (dependencyAnalysis.highSeverityCount !== null && dependencyAnalysis.highSeverityCount > 0) { riskScore += 3; riskFactors.push(`${dependencyAnalysis.highSeverityCount} high severity vulnerabilities found.`); } if (dependencyAnalysis.mediumSeverityCount !== null && dependencyAnalysis.mediumSeverityCount > 0) { riskScore += 2; riskFactors.push(`${dependencyAnalysis.mediumSeverityCount} medium severity vulnerabilities found.`); } if (dependencyAnalysis.lowSeverityCount !== null && dependencyAnalysis.lowSeverityCount > 0) { riskScore += 1; riskFactors.push(`${dependencyAnalysis.lowSeverityCount} low severity vulnerabilities found.`); } if (dependencyAnalysis.hasDependencies && !dependencyAnalysis.alertsEnabled) { riskScore += 1; riskFactors.push('Repository has dependencies but vulnerability alerts are not enabled.'); } // Add outdated dependencies risk factors if (dependencyAnalysis.majorOutdatedCount > 5) { riskScore += 3; riskFactors.push(`Many dependencies are severely outdated (${dependencyAnalysis.majorOutdatedCount} major versions behind).`); } else if (dependencyAnalysis.majorOutdatedCount > 0) { riskScore += 2; riskFactors.push(`Some dependencies are severely outdated (${dependencyAnalysis.majorOutdatedCount} major versions behind).`); } if (dependencyAnalysis.minorOutdatedCount > 10) { riskScore += 1; riskFactors.push(`Many dependencies need minor version updates (${dependencyAnalysis.minorOutdatedCount} minor versions behind).`); } // Adjust risk rating scale for the new factors let riskRating = 'Low'; if (riskScore > 15) { // Adjusted threshold riskRating = 'High'; } else if (riskScore > 10) { // Adjusted threshold riskRating = 'Medium'; } // --- Construct Markdown Summary --- const markdownSummary = `## GitHub Repository Analysis: ${owner}/${repo} ${readmeExcerpt ? ` ### Project Description ${readmeExcerpt} [View Full README](${readmeUrl}) ` : ''} ### Sustainability Assessment | Metric | Value | |--------|-------| | Number of Contributors | ${numberOfContributors} | | Avg. Contributor Account Age | ${averageContributorAgeInYears ? `${averageContributorAgeInYears} years` : 'N/A'} | | Project Age | ${ageInDays} days (Created on ${createdAt.toLocaleDateString()}) | | Recent Commits | ${numberOfCommits} commits found in the last 100 | | Last Activity | ${daysSinceLastCommit !== null ? `${daysSinceLastCommit} days ago (${lastCommitDate ? lastCommitDate.toLocaleDateString() : 'N/A'})` : 'N/A'} | | Contributing Guidelines | ${hasContributingGuidelines ? '✅ Present' : '❌ Missing'} | | Long-Lived PRs (>90 days) | ${longLivingPulls.length} | | Long-Lived Issues (>180 days) | ${longLivingIssues.length} | ### Security Assessment | Metric | Value | |--------|-------| | Security Policy | ${hasSecurityPolicy ? '✅ Present' : '❌ Missing'} | | Code Complexity | ${codeComplexity} | | Last Update | ${updatedAt.toLocaleDateString()} | ### License Information | Metric | Value | |--------|-------| | License | ${license} | | License Risk | ${licenseRisk} | ### Dependency Analysis | Metric | Value | |--------|-------| | Dependency Files | ${hasDependencyFile ? '✅ Found' : '❌ Not Found'} | | Total Dependencies | ${dependencyAnalysis.dependenciesCount > 0 ? dependencyAnalysis.dependenciesCount : 'N/A'} | | Major Version Outdated | ${dependencyAnalysis.majorOutdatedCount > 0 ? `⚠️ ${dependencyAnalysis.majorOutdatedCount}` : dependencyAnalysis.parsedDependencies.length > 0 ? '✅ 0' : 'N/A'} | | Minor Version Outdated | ${dependencyAnalysis.minorOutdatedCount > 0 ? `ℹ️ ${dependencyAnalysis.minorOutdatedCount}` : dependencyAnalysis.parsedDependencies.length > 0 ? '✅ 0' : 'N/A'} | | Vulnerability Alerts | ${dependencyAnalysis.alertsEnabled ? '✅ Enabled' : '❌ Disabled/Not Available'} | | High Severity Vulnerabilities | ${dependencyAnalysis.highSeverityCount !== null ? dependencyAnalysis.highSeverityCount : 'N/A'} | | Medium Severity Vulnerabilities | ${dependencyAnalysis.mediumSeverityCount !== null ? dependencyAnalysis.mediumSeverityCount : 'N/A'} | | Low Severity Vulnerabilities | ${dependencyAnalysis.lowSeverityCount !== null ? dependencyAnalysis.lowSeverityCount : 'N/A'} | | Vulnerability Source | ${dependencyAnalysis.vulnerabilitySource || 'N/A'} | ${dependencyAnalysis.vulnerablePackages.length > 0 ? ` #### Vulnerable Dependencies | Package | Severity | Fixed In | ID | |---------|----------|---------|-----| ${dependencyAnalysis.vulnerablePackages .map(pkg => `| ${pkg.name} | ${pkg.severity} | ${pkg.fixedIn} | ${pkg.id || 'N/A'} |`) .join('\n')} ${dependencyAnalysis.vulnerablePackages .map(pkg => (pkg.details ? `**${pkg.name}**: ${pkg.details}` : '')) .filter(Boolean) .join('\n\n')} ` : ''} ${dependencyAnalysis.outdatedDependencies.length > 0 ? ` #### Outdated Dependencies | Package | Current Version | Latest Version | Update Urgency | |---------|----------------|---------------|---------------| ${dependencyAnalysis.outdatedDependencies .map(dep => `| ${dep.name} | ${dep.currentVersion} | ${dep.latestVersion} | ${dep.updateUrgency === 'high' ? '🔴 High' : dep.updateUrgency === 'medium' ? '🟠 Medium' : '🟡 Low'} |`) .join('\n')} ` : ''} ${dependencyAnalysis.parsedDependencies.length > 0 ? ` #### Top Dependencies | Package | Version | |---------|---------| ${dependencyAnalysis.parsedDependencies.map(dep => `| ${dep.name} | ${dep.version} |`).join('\n')} ` : ''} ### Development Quality | Metric | Value | |--------|-------| | Dependency Management | ${hasDependencyFile ? '✅ Present' : '❌ Missing'} | | Test Coverage | ${hasTestDirectory ? '✅ Tests Found' : '❌ No Tests Found'} | | CI/CD Setup | ${hasCiSetup ? '✅ Present' : '❌ Missing'} | | Code Quality Tools | ${hasCodeQualityTools ? '✅ Present' : '❌ Missing'} | | Documentation | ${hasReadme ? (hasDocDirectory ? '✅ Extensive' : '✅ Basic') : '❌ Missing'} | ### Community Health | Metric | Value | |--------|-------| | Stars | ${starCount} | | Forks | ${forkCount} | | Watchers | ${watcherCount} | | Open Issues | ${openIssuesCount} | | Average Response Time | ${responseTimeMetrics ? `${responseTimeMetrics.averageResponseHours} hours (Sample: ${responseTimeMetrics.sampleSize})` : 'N/A'} | ### Release Practic