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
2 changes: 1 addition & 1 deletion .github/workflows/qodana.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
if: github.event_name != 'pull_request'
uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
with:
sarif_file: artifacts/ci/qodana/qodana.sarif.json
sarif_file: artifacts/ci/qodana/qodana.upload.sarif.json

- name: Upload Artifact
if: always()
Expand Down
7 changes: 6 additions & 1 deletion tools/ci/bin/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,8 @@ run_pr_labeling() {
run_qodana_contract() {
build_validators
local sarif_path="${OUT_DIR}/qodana.sarif.json"
if ! ci_run_capture "Qodana contract validator" dotnet "${ROOT_DIR}/tools/ci/checks/QodanaContractValidator/bin/Release/net10.0/QodanaContractValidator.dll" --sarif "$sarif_path"; then
local filtered_sarif_path="${OUT_DIR}/qodana.upload.sarif.json"
if ! ci_run_capture "Qodana contract validator" dotnet "${ROOT_DIR}/tools/ci/checks/QodanaContractValidator/bin/Release/net10.0/QodanaContractValidator.dll" --sarif "$sarif_path" --filtered-sarif-out "$filtered_sarif_path"; then
if log_contains_code "CI-QODANA-001"; then
ci_result_add_violation "CI-QODANA-001" "fail" "QODANA_TOKEN missing" "$CI_RAW_LOG"
elif log_contains_code "CI-QODANA-002"; then
Expand All @@ -467,6 +468,10 @@ run_qodana_contract() {
fi
return 1
fi
if [[ ! -f "${filtered_sarif_path}" ]]; then
ci_result_add_violation "CI-QODANA-003" "fail" "Filtered SARIF output missing" "$CI_RAW_LOG" "${filtered_sarif_path}"
return 1
fi
ci_result_append_summary "Qodana contract validation completed."
}

Expand Down
26 changes: 23 additions & 3 deletions tools/ci/check-code-scanning-tools-zero.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,39 @@ fi

# Gate strategy:
# - Default: enforce 0 open alerts on main (prevents unrelated merges while main is red).
# - Exception: PRs labeled "area:qodana" are allowed to validate against the PR ref, so the cleanup PR itself can merge.
# - Exception: Qodana-cleanup PRs validate against the PR ref, so the cleanup PR itself can merge.
# Detection is fail-safe and deterministic:
# 1) explicit label "area:qodana", OR
# 2) changed files include qodana paths (independent of label-job timing).
EVENT_NAME="${GITHUB_EVENT_NAME:-}"
QUERY_REF=""
if [[ "${EVENT_NAME}" == "pull_request" && -n "${GITHUB_EVENT_PATH:-}" && -f "${GITHUB_EVENT_PATH:-}" ]]; then
pr_number="$(jq -r '.pull_request.number // empty' "${GITHUB_EVENT_PATH}")"
has_qodana_label="false"
if jq -r '.pull_request.labels[].name // empty' "${GITHUB_EVENT_PATH}" | grep -Fxq -- "area:qodana"; then
has_qodana_label="true"
fi

has_qodana_changes="false"
if [[ -n "${pr_number}" ]]; then
files_json="${OUT_DIR}/pr-files.json"
if gh api "repos/${REPO}/pulls/${pr_number}/files?per_page=100" --paginate > "${files_json}" 2>> "${RAW_LOG}"; then
if jq -r '.[].filename' "${files_json}" | grep -Eiq '^(\.qodana/|qodana\.ya?ml$|\.github/workflows/qodana\.yml$)'; then
has_qodana_changes="true"
fi
else
log "WARN: PR files konnten nicht geladen werden; fallback auf Label-basierte Erkennung."
fi
fi

if [[ "${has_qodana_label}" == "true" || "${has_qodana_changes}" == "true" ]]; then
QUERY_REF="${GITHUB_REF:-}"
if [[ -z "${QUERY_REF}" ]]; then
pr_number="$(jq -r '.pull_request.number // empty' "${GITHUB_EVENT_PATH}")"
if [[ -n "${pr_number}" ]]; then
QUERY_REF="refs/pull/${pr_number}/merge"
fi
fi
log "INFO: PR hat Label area:qodana -> pruefe Code-Scanning Alerts fuer ref=${QUERY_REF:-<unset>}"
log "INFO: Qodana-PR erkannt (label=${has_qodana_label}, files=${has_qodana_changes}) -> pruefe Code-Scanning Alerts fuer ref=${QUERY_REF:-<unset>}"
else
QUERY_REF="refs/heads/main"
log "INFO: PR ohne Label area:qodana -> pruefe Code-Scanning Alerts fuer ref=${QUERY_REF}"
Expand Down
130 changes: 129 additions & 1 deletion tools/ci/checks/QodanaContractValidator/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json;
using System.Text.Json.Nodes;

var nonBlockingHighRuleIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
Expand All @@ -13,12 +14,17 @@

var argsList = args.ToList();
string? sarifPath = null;
string? filteredSarifOutPath = null;
for (var i = 0; i < argsList.Count; i++)
{
if (argsList[i] == "--sarif" && i + 1 < argsList.Count)
{
sarifPath = argsList[++i];
}
else if (argsList[i] == "--filtered-sarif-out" && i + 1 < argsList.Count)
{
filteredSarifOutPath = argsList[++i];
}
}

if (string.IsNullOrWhiteSpace(sarifPath))
Expand All @@ -42,7 +48,8 @@

try
{
using var doc = JsonDocument.Parse(File.ReadAllText(sarifPath));
var sarifText = File.ReadAllText(sarifPath);
using var doc = JsonDocument.Parse(sarifText);
if (!doc.RootElement.TryGetProperty("runs", out var runs) || runs.ValueKind != JsonValueKind.Array)
{
Console.Error.WriteLine("CI-QODANA-003: SARIF missing runs[] array");
Expand Down Expand Up @@ -106,6 +113,15 @@
Console.Error.WriteLine($"QODANA_FINDING|severity={finding.Severity}|rule_id={finding.RuleId}|location={finding.Location}|message={finding.Message}");
}

if (!string.IsNullOrWhiteSpace(filteredSarifOutPath))
{
if (!TryWriteFilteredSarif(sarifText, filteredSarifOutPath!, nonBlockingHighRuleIds))
{
Console.Error.WriteLine("CI-QODANA-003: unable to write filtered SARIF output");
return 1;
}
}

if (blockingFindings.Count > 0)
{
Console.Error.WriteLine($"CI-QODANA-004: blocking findings detected at severity High+ ({blockingFindings.Count})");
Expand Down Expand Up @@ -229,4 +245,116 @@ static string ExtractSeverity(JsonElement result)
return value.GetString();
}

static bool TryWriteFilteredSarif(string sarifText, string outputPath, ISet<string> nonBlockingHighRuleIds)
{
JsonNode? rootNode;
try
{
rootNode = JsonNode.Parse(sarifText);
}
catch
{
return false;
}

if (rootNode is not JsonObject rootObject)
{
return false;
}

if (rootObject["runs"] is not JsonArray runsArray)
{
return false;
}

foreach (var runNode in runsArray)
{
if (runNode is not JsonObject runObject)
{
continue;
}

if (runObject["results"] is not JsonArray resultsArray)
{
continue;
}

var filteredResults = new JsonArray();
foreach (var resultNode in resultsArray)
{
if (resultNode is not JsonObject resultObject)
{
continue;
}

var severity = ExtractSeverityFromNode(resultObject);
var ruleId = TryGetNodeString(resultObject["ruleId"]) ?? "UNKNOWN";

var isBlocking = SeverityRank(severity) >= SeverityRank("High") &&
!nonBlockingHighRuleIds.Contains(ruleId);
if (isBlocking)
{
filteredResults.Add(resultObject.DeepClone());
}
}

runObject["results"] = filteredResults;
}

var outputDirectory = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrWhiteSpace(outputDirectory))
{
Directory.CreateDirectory(outputDirectory);
}

File.WriteAllText(outputPath, rootObject.ToJsonString(new JsonSerializerOptions
{
WriteIndented = false
}));
Console.WriteLine($"QODANA_FILTERED_SARIF|path={outputPath}");
return true;
}

static string ExtractSeverityFromNode(JsonObject resultObject)
{
if (resultObject["properties"] is JsonObject propertiesObject)
{
var qodanaSeverity = TryGetNodeString(propertiesObject["qodanaSeverity"]);
if (!string.IsNullOrWhiteSpace(qodanaSeverity))
{
return qodanaSeverity!;
}
}

var level = TryGetNodeString(resultObject["level"]);
return level?.ToLowerInvariant() switch
{
"error" => "High",
"warning" => "Moderate",
"note" => "Low",
_ => "Unknown"
};
}

static string? TryGetNodeString(JsonNode? node)
{
if (node is null)
{
return null;
}

try
{
return node.GetValue<string>();
}
catch (InvalidOperationException)
{
return null;
}
catch (FormatException)
{
return null;
}
}

internal sealed record Finding(string Severity, string RuleId, string Message, string Location);
1 change: 1 addition & 0 deletions tools/versioning/compute-pr-labels.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const AREA_RULES = [
{ prefix: '.github/workflows/', label: 'area:pipeline' },
{ prefix: '.qodana/', label: 'area:qodana' },
{ exact: 'qodana.yaml', label: 'area:qodana' },
{ exact: '.github/workflows/qodana.yml', label: 'area:qodana' },
{ prefix: 'src/FileTypeDetection/Infrastructure/Archive', label: 'area:archive' },
{ prefix: 'src/FileTypeDetection/ArchiveProcessing', label: 'area:archive' },
{ exact: 'src/FileTypeDetection/EvidenceHashing.vb', label: 'area:hashing' },
Expand Down
Loading