Skip to content
Merged
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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
2 changes: 1 addition & 1 deletion src/contractguard/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""ContractGuard core package."""

__version__ = "1.1.0"
__version__ = "1.2.0"
16 changes: 9 additions & 7 deletions src/contractguard/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
}
Expand Down
23 changes: 21 additions & 2 deletions tests/test_reporter.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
24 changes: 23 additions & 1 deletion vscode-src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -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
};
Expand Down
13 changes: 12 additions & 1 deletion vscode-src/findingsTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +23 to +31
}

class SeverityGroupNode extends vscode.TreeItem {
constructor(
public readonly severity: Severity,
Expand All @@ -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(
Expand Down
Loading