diff --git a/package-lock.json b/package-lock.json index f87d8bc..81f52c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "contractguard", - "version": "1.1.0", + "name": "contract-guard", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "contractguard", - "version": "1.1.0", + "name": "contract-guard", + "version": "1.2.0", "license": "Apache-2.0", "devDependencies": { "@types/node": "^20.16.1", diff --git a/package.json b/package.json index 3096189..d99d166 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "contract-guard", "displayName": "contract-guard", "description": "Security analysis for code, config, Dockerfiles, data payloads, and dependencies.", - "version": "1.1.0", + "version": "1.2.0", "publisher": "BlackplaneSystems", "license": "Apache-2.0", "icon": "media/icon.png", @@ -158,7 +158,7 @@ }, "scripts": { "build": "tsc -p ./tsconfig.json", - "package": "node -e \"require('fs').mkdirSync('dist-vsix',{recursive:true})\" && vsce package --out dist-vsix/contractguard-1.1.0.vsix", + "package": "node -e \"require('fs').mkdirSync('dist-vsix',{recursive:true})\" && vsce package --out dist-vsix/contractguard-1.2.0.vsix", "prepackage": "npm run build" }, "devDependencies": { diff --git a/pyproject.toml b/pyproject.toml index 00a2766..d7b2f2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "contractguard" -version = "1.1.0" +version = "1.2.0" description = "ContractGuard security analysis core for VS Code and CI workflows." readme = "README.md" license = {text = "Apache-2.0"} diff --git a/src/contractguard/__init__.py b/src/contractguard/__init__.py index 91e3269..0f9bd50 100644 --- a/src/contractguard/__init__.py +++ b/src/contractguard/__init__.py @@ -1,3 +1,3 @@ """ContractGuard core package.""" -__version__ = "1.1.0" +__version__ = "1.2.0" diff --git a/src/contractguard/reporter.py b/src/contractguard/reporter.py index 7ba3719..34b4ed3 100644 --- a/src/contractguard/reporter.py +++ b/src/contractguard/reporter.py @@ -175,14 +175,16 @@ def render_sarif_report( rule_def["fullDescription"] = {"text": f"Attack vector: {finding.attack_vector}"} rules.append(rule_def) - file_path = finding.location.split(":")[0] if finding.location else "" + file_path = finding.location or "" line = 1 - if ":" in finding.location: + if finding.location and ":" in finding.location: parts = finding.location.rsplit(":", 1) - try: - line = int(parts[1]) - except ValueError: - line = 1 + if len(parts) == 2: + try: + line = int(parts[1]) + file_path = parts[0] + except ValueError: + line = 1 result: dict[str, Any] = { "ruleId": finding.rule_id, @@ -209,7 +211,7 @@ def render_sarif_report( "tool": { "driver": { "name": "ContractGuard", - "version": "1.1.0", + "version": "1.2.0", "informationUri": "https://github.com/Blackplane-Systems/contractguard", "rules": rules, } diff --git a/tests/test_reporter.py b/tests/test_reporter.py index 0e78dd1..da76f08 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -1,7 +1,7 @@ -"""Tests for the HTML reporter.""" +"""Tests for report generation.""" from contractguard.engine import Finding, Severity -from contractguard.reporter import render_html_report +from contractguard.reporter import render_html_report, render_sarif_report class TestRenderHtmlReport: @@ -32,3 +32,22 @@ def test_contains_metadata(self): html = render_html_report([], analyzer_type="regex", source_path="patterns.txt") assert "regex" in html assert "patterns.txt" in html + + def test_sarif_preserves_windows_drive_paths(self): + findings = [ + Finding( + rule_id="TEST002", + rule_name="test", + severity=Severity.WARNING, + description="Needs attention", + explanation="Matched", + suggestion="Fix it", + location=r"C:\repo\.env:12", + context="API_KEY=example", + ) + ] + + sarif = render_sarif_report(findings) + location = sarif["runs"][0]["results"][0]["locations"][0]["physicalLocation"] + assert location["artifactLocation"]["uri"] == "C:/repo/.env" + assert location["region"]["startLine"] == 12 diff --git a/vscode-src/extension.ts b/vscode-src/extension.ts index 3d175c0..338dbb0 100644 --- a/vscode-src/extension.ts +++ b/vscode-src/extension.ts @@ -24,6 +24,21 @@ const supportedExtensions = new Set([ '.dockerfile' ]); +function riskSummaryForGrade(grade: string): string { + switch (grade) { + case 'A': + return 'Minimal risk. Good security posture.'; + case 'B': + return 'Low risk. A few issues to address before production.'; + case 'C': + return 'Moderate risk. Several issues need attention.'; + case 'D': + return 'High risk. Significant security issues detected.'; + default: + return 'CRITICAL RISK. Deployment must be blocked until issues are resolved.'; + } +} + class ContractGuardController implements vscode.Disposable { private readonly diagnostics = vscode.languages.createDiagnosticCollection('contractguard'); private readonly tree = new FindingsTreeDataProvider(); @@ -317,7 +332,13 @@ class ContractGuardController implements vscode.Disposable { warning_count: findings.filter((item) => item.severity === 'warning').length, info_count: findings.filter((item) => item.severity === 'info').length }; - const attackSurface = [...new Set(findings.flatMap((item) => score.attack_surface.includes(item.attack_vector) ? [item.attack_vector] : []))]; + const attackSurface = [ + ...new Set( + findings + .map((item) => item.attack_vector) + .filter((attackVector) => typeof attackVector === 'string' && attackVector.trim().length > 0) + ) + ]; const topRisks = [...new Set(findings.map((item) => `[${item.severity.toUpperCase()}] ${item.description}`))].slice(0, 5); const scoreValue = Math.max( 0, @@ -337,6 +358,7 @@ class ContractGuardController implements vscode.Disposable { score: counts.block_count > 0 ? Math.min(scoreValue, 15) : scoreValue, ...counts, total_findings: findings.length, + risk_summary: riskSummaryForGrade(grade), attack_surface: attackSurface, top_risks: topRisks }; diff --git a/vscode-src/findingsTree.ts b/vscode-src/findingsTree.ts index 8bd4aba..277019c 100644 --- a/vscode-src/findingsTree.ts +++ b/vscode-src/findingsTree.ts @@ -20,6 +20,17 @@ function severityIcon(severity: Severity): vscode.ThemeIcon { } } +function locationBasename(location: string): string { + const separator = location.lastIndexOf(':'); + if (separator > 1) { + const suffix = location.slice(separator + 1); + if (/^\d+$/.test(suffix)) { + return path.basename(location.slice(0, separator)); + } + } + return path.basename(location); +} + class SeverityGroupNode extends vscode.TreeItem { constructor( public readonly severity: Severity, @@ -33,7 +44,7 @@ class SeverityGroupNode extends vscode.TreeItem { class FindingNode extends vscode.TreeItem { constructor(public readonly finding: Finding) { - const basename = finding.location ? path.basename(finding.location.split(':')[0]) : finding.rule_id; + const basename = finding.location ? locationBasename(finding.location) : finding.rule_id; super(`${finding.rule_id} ${basename}`, vscode.TreeItemCollapsibleState.None); this.description = finding.description; this.tooltip = new vscode.MarkdownString(