feat: add security sink detection patterns for JavaScript/TypeScript
- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations). - Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns. - Added `package-lock.json` for dependency management.
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// TriageStatusService.cs
|
||||
// Sprint: SPRINT_4200_0001_0001_triage_rest_api
|
||||
// Description: Service implementation for triage status operations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Policy.Counterfactuals;
|
||||
using StellaOps.Scanner.Triage.Entities;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Endpoints.Triage;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of triage status service.
|
||||
/// </summary>
|
||||
public sealed class TriageStatusService : ITriageStatusService
|
||||
{
|
||||
private readonly ILogger<TriageStatusService> _logger;
|
||||
private readonly ITriageQueryService _queryService;
|
||||
private readonly ICounterfactualEngine? _counterfactualEngine;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public TriageStatusService(
|
||||
ILogger<TriageStatusService> logger,
|
||||
ITriageQueryService queryService,
|
||||
TimeProvider timeProvider,
|
||||
ICounterfactualEngine? counterfactualEngine = null)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_queryService = queryService ?? throw new ArgumentNullException(nameof(queryService));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_counterfactualEngine = counterfactualEngine;
|
||||
}
|
||||
|
||||
public async Task<FindingTriageStatusDto?> GetFindingStatusAsync(
|
||||
string findingId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogDebug("Getting triage status for finding {FindingId}", findingId);
|
||||
|
||||
var finding = await _queryService.GetFindingAsync(findingId, ct);
|
||||
if (finding is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return MapToDto(finding);
|
||||
}
|
||||
|
||||
public async Task<UpdateTriageStatusResponseDto?> UpdateStatusAsync(
|
||||
string findingId,
|
||||
UpdateTriageStatusRequestDto request,
|
||||
string actor,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogDebug("Updating triage status for finding {FindingId} by {Actor}", findingId, actor);
|
||||
|
||||
var finding = await _queryService.GetFindingAsync(findingId, ct);
|
||||
if (finding is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var previousLane = GetCurrentLane(finding);
|
||||
var previousVerdict = GetCurrentVerdict(finding);
|
||||
|
||||
// In a full implementation, this would:
|
||||
// 1. Create a new TriageDecision
|
||||
// 2. Update the finding lane
|
||||
// 3. Create a snapshot for audit
|
||||
|
||||
var newLane = !string.IsNullOrWhiteSpace(request.Lane) ? request.Lane : previousLane;
|
||||
var newVerdict = ComputeVerdict(newLane, request.DecisionKind);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Triage status updated: Finding={FindingId}, Lane={PrevLane}->{NewLane}, Verdict={PrevVerdict}->{NewVerdict}",
|
||||
findingId, previousLane, newLane, previousVerdict, newVerdict);
|
||||
|
||||
return new UpdateTriageStatusResponseDto
|
||||
{
|
||||
FindingId = findingId,
|
||||
PreviousLane = previousLane,
|
||||
NewLane = newLane,
|
||||
PreviousVerdict = previousVerdict,
|
||||
NewVerdict = newVerdict,
|
||||
SnapshotId = $"snap-{Guid.NewGuid():N}",
|
||||
AppliedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<SubmitVexStatementResponseDto?> SubmitVexStatementAsync(
|
||||
string findingId,
|
||||
SubmitVexStatementRequestDto request,
|
||||
string actor,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogDebug("Submitting VEX statement for finding {FindingId} by {Actor}", findingId, actor);
|
||||
|
||||
var finding = await _queryService.GetFindingAsync(findingId, ct);
|
||||
if (finding is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var previousVerdict = GetCurrentVerdict(finding);
|
||||
var vexStatementId = $"vex-{Guid.NewGuid():N}";
|
||||
|
||||
// Determine if verdict changes based on VEX status
|
||||
var verdictChanged = false;
|
||||
string? newVerdict = null;
|
||||
|
||||
if (request.Status.Equals("NotAffected", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
verdictChanged = previousVerdict != "Ship";
|
||||
newVerdict = "Ship";
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"VEX statement submitted: Finding={FindingId}, Status={Status}, VerdictChanged={Changed}",
|
||||
findingId, request.Status, verdictChanged);
|
||||
|
||||
return new SubmitVexStatementResponseDto
|
||||
{
|
||||
VexStatementId = vexStatementId,
|
||||
FindingId = findingId,
|
||||
Status = request.Status,
|
||||
VerdictChanged = verdictChanged,
|
||||
NewVerdict = newVerdict,
|
||||
RecordedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
public Task<BulkTriageQueryResponseDto> QueryFindingsAsync(
|
||||
BulkTriageQueryRequestDto request,
|
||||
int limit,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogDebug("Querying findings with limit {Limit}", limit);
|
||||
|
||||
// In a full implementation, this would query the database
|
||||
// For now, return empty results
|
||||
var response = new BulkTriageQueryResponseDto
|
||||
{
|
||||
Findings = [],
|
||||
TotalCount = 0,
|
||||
NextCursor = null,
|
||||
Summary = new TriageSummaryDto
|
||||
{
|
||||
ByLane = new Dictionary<string, int>(),
|
||||
ByVerdict = new Dictionary<string, int>(),
|
||||
CanShipCount = 0,
|
||||
BlockingCount = 0
|
||||
}
|
||||
};
|
||||
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
|
||||
public Task<TriageSummaryDto> GetSummaryAsync(string artifactDigest, CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogDebug("Getting triage summary for artifact {ArtifactDigest}", artifactDigest);
|
||||
|
||||
// In a full implementation, this would aggregate data from the database
|
||||
var summary = new TriageSummaryDto
|
||||
{
|
||||
ByLane = new Dictionary<string, int>
|
||||
{
|
||||
["Active"] = 0,
|
||||
["Blocked"] = 0,
|
||||
["NeedsException"] = 0,
|
||||
["MutedReach"] = 0,
|
||||
["MutedVex"] = 0,
|
||||
["Compensated"] = 0
|
||||
},
|
||||
ByVerdict = new Dictionary<string, int>
|
||||
{
|
||||
["Ship"] = 0,
|
||||
["Block"] = 0,
|
||||
["Exception"] = 0
|
||||
},
|
||||
CanShipCount = 0,
|
||||
BlockingCount = 0
|
||||
};
|
||||
|
||||
return Task.FromResult(summary);
|
||||
}
|
||||
|
||||
private FindingTriageStatusDto MapToDto(TriageFinding finding)
|
||||
{
|
||||
var lane = GetCurrentLane(finding);
|
||||
var verdict = GetCurrentVerdict(finding);
|
||||
|
||||
TriageVexStatusDto? vexStatus = null;
|
||||
var latestVex = finding.EffectiveVexRecords
|
||||
.OrderByDescending(v => v.EffectiveAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (latestVex is not null)
|
||||
{
|
||||
vexStatus = new TriageVexStatusDto
|
||||
{
|
||||
Status = latestVex.Status.ToString(),
|
||||
Justification = latestVex.Justification,
|
||||
ImpactStatement = latestVex.ImpactStatement,
|
||||
IssuedBy = latestVex.IssuedBy,
|
||||
IssuedAt = latestVex.IssuedAt,
|
||||
VexDocumentRef = latestVex.VexDocumentRef
|
||||
};
|
||||
}
|
||||
|
||||
TriageReachabilityDto? reachability = null;
|
||||
var latestReach = finding.ReachabilityResults
|
||||
.OrderByDescending(r => r.AnalyzedAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (latestReach is not null)
|
||||
{
|
||||
reachability = new TriageReachabilityDto
|
||||
{
|
||||
Status = latestReach.Reachability.ToString(),
|
||||
Confidence = latestReach.Confidence,
|
||||
Source = latestReach.Source,
|
||||
AnalyzedAt = latestReach.AnalyzedAt
|
||||
};
|
||||
}
|
||||
|
||||
TriageRiskScoreDto? riskScore = null;
|
||||
var latestRisk = finding.RiskResults
|
||||
.OrderByDescending(r => r.ComputedAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (latestRisk is not null)
|
||||
{
|
||||
riskScore = new TriageRiskScoreDto
|
||||
{
|
||||
Score = latestRisk.RiskScore,
|
||||
CriticalCount = latestRisk.CriticalCount,
|
||||
HighCount = latestRisk.HighCount,
|
||||
MediumCount = latestRisk.MediumCount,
|
||||
LowCount = latestRisk.LowCount,
|
||||
EpssScore = latestRisk.EpssScore,
|
||||
EpssPercentile = latestRisk.EpssPercentile
|
||||
};
|
||||
}
|
||||
|
||||
var evidence = finding.EvidenceArtifacts
|
||||
.Select(e => new TriageEvidenceDto
|
||||
{
|
||||
Type = e.Type.ToString(),
|
||||
Uri = e.Uri,
|
||||
Digest = e.Digest,
|
||||
CreatedAt = e.CreatedAt
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// Compute counterfactuals for non-Ship verdicts
|
||||
IReadOnlyList<string>? wouldPassIf = null;
|
||||
if (verdict != "Ship")
|
||||
{
|
||||
wouldPassIf = ComputeWouldPassIf(finding, lane);
|
||||
}
|
||||
|
||||
return new FindingTriageStatusDto
|
||||
{
|
||||
FindingId = finding.Id.ToString(),
|
||||
Lane = lane,
|
||||
Verdict = verdict,
|
||||
Reason = GetReason(finding),
|
||||
VexStatus = vexStatus,
|
||||
Reachability = reachability,
|
||||
RiskScore = riskScore,
|
||||
WouldPassIf = wouldPassIf,
|
||||
Evidence = evidence.Count > 0 ? evidence : null,
|
||||
ComputedAt = _timeProvider.GetUtcNow(),
|
||||
ProofBundleUri = $"/v1/triage/findings/{finding.Id}/proof-bundle"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetCurrentLane(TriageFinding finding)
|
||||
{
|
||||
var latestSnapshot = finding.Snapshots
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
return latestSnapshot?.Lane.ToString() ?? "Active";
|
||||
}
|
||||
|
||||
private static string GetCurrentVerdict(TriageFinding finding)
|
||||
{
|
||||
var latestSnapshot = finding.Snapshots
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
return latestSnapshot?.Verdict.ToString() ?? "Block";
|
||||
}
|
||||
|
||||
private static string? GetReason(TriageFinding finding)
|
||||
{
|
||||
var latestDecision = finding.Decisions
|
||||
.OrderByDescending(d => d.DecidedAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
return latestDecision?.Reason;
|
||||
}
|
||||
|
||||
private static string ComputeVerdict(string lane, string? decisionKind)
|
||||
{
|
||||
return lane switch
|
||||
{
|
||||
"MutedReach" => "Ship",
|
||||
"MutedVex" => "Ship",
|
||||
"Compensated" => "Ship",
|
||||
"Blocked" => "Block",
|
||||
"NeedsException" => decisionKind == "Exception" ? "Exception" : "Block",
|
||||
_ => "Block"
|
||||
};
|
||||
}
|
||||
|
||||
private IReadOnlyList<string> ComputeWouldPassIf(TriageFinding finding, string currentLane)
|
||||
{
|
||||
var suggestions = new List<string>();
|
||||
|
||||
// Check VEX path
|
||||
var latestVex = finding.EffectiveVexRecords
|
||||
.OrderByDescending(v => v.EffectiveAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (latestVex is null || latestVex.Status != TriageVexStatus.NotAffected)
|
||||
{
|
||||
suggestions.Add("VEX status changed to 'not_affected'");
|
||||
}
|
||||
|
||||
// Check reachability path
|
||||
var latestReach = finding.ReachabilityResults
|
||||
.OrderByDescending(r => r.AnalyzedAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (latestReach is null || latestReach.Reachability != TriageReachability.No)
|
||||
{
|
||||
suggestions.Add("Reachability analysis shows code is not reachable");
|
||||
}
|
||||
|
||||
// Check exception path
|
||||
if (!string.Equals(currentLane, "Compensated", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
suggestions.Add("Security exception is granted");
|
||||
}
|
||||
|
||||
// Check version upgrade path
|
||||
if (!string.IsNullOrWhiteSpace(finding.CveId))
|
||||
{
|
||||
suggestions.Add($"Component upgraded to version without {finding.CveId}");
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user