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,131 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Explainability.Falsifiability;
|
||||
|
||||
/// <summary>
|
||||
/// Represents criteria that would falsify (disprove) a vulnerability finding.
|
||||
/// Answers the question: "What would prove this finding wrong?"
|
||||
/// </summary>
|
||||
public sealed record FalsifiabilityCriteria
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this criteria set.
|
||||
/// </summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The finding ID these criteria apply to.
|
||||
/// </summary>
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Individual criteria that would disprove the finding.
|
||||
/// </summary>
|
||||
public ImmutableArray<FalsificationCriterion> Criteria { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Overall falsifiability status.
|
||||
/// </summary>
|
||||
public FalsifiabilityStatus Status { get; init; } = FalsifiabilityStatus.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable summary of what would disprove this finding.
|
||||
/// </summary>
|
||||
public string? Summary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When these criteria were generated.
|
||||
/// </summary>
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single criterion that would falsify a finding.
|
||||
/// </summary>
|
||||
/// <param name="Type">The type of falsification check</param>
|
||||
/// <param name="Description">Human-readable description of the criterion</param>
|
||||
/// <param name="CheckExpression">Machine-evaluable expression (e.g., CEL, Rego)</param>
|
||||
/// <param name="Evidence">Evidence that supports or refutes this criterion</param>
|
||||
/// <param name="Status">Current evaluation status</param>
|
||||
public sealed record FalsificationCriterion(
|
||||
FalsificationType Type,
|
||||
string Description,
|
||||
string? CheckExpression,
|
||||
string? Evidence,
|
||||
CriterionStatus Status
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Types of falsification criteria.
|
||||
/// </summary>
|
||||
public enum FalsificationType
|
||||
{
|
||||
/// <summary>Package is not actually installed</summary>
|
||||
PackageNotPresent,
|
||||
|
||||
/// <summary>Vulnerable version is not the installed version</summary>
|
||||
VersionMismatch,
|
||||
|
||||
/// <summary>Vulnerable code path is not reachable</summary>
|
||||
CodeUnreachable,
|
||||
|
||||
/// <summary>Required feature/function is disabled</summary>
|
||||
FeatureDisabled,
|
||||
|
||||
/// <summary>Mitigation is in place (ASLR, stack canaries, etc.)</summary>
|
||||
MitigationPresent,
|
||||
|
||||
/// <summary>Network exposure required but not present</summary>
|
||||
NoNetworkExposure,
|
||||
|
||||
/// <summary>Required privileges not available</summary>
|
||||
InsufficientPrivileges,
|
||||
|
||||
/// <summary>Vulnerability is already patched</summary>
|
||||
PatchApplied,
|
||||
|
||||
/// <summary>Configuration prevents exploitation</summary>
|
||||
ConfigurationPrevents,
|
||||
|
||||
/// <summary>Runtime environment prevents exploitation</summary>
|
||||
RuntimePrevents
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status of a falsification criterion evaluation.
|
||||
/// </summary>
|
||||
public enum CriterionStatus
|
||||
{
|
||||
/// <summary>Not yet evaluated</summary>
|
||||
Pending,
|
||||
|
||||
/// <summary>Criterion is satisfied (finding is falsified)</summary>
|
||||
Satisfied,
|
||||
|
||||
/// <summary>Criterion is not satisfied (finding stands)</summary>
|
||||
NotSatisfied,
|
||||
|
||||
/// <summary>Could not be evaluated (insufficient data)</summary>
|
||||
Inconclusive
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overall falsifiability status.
|
||||
/// </summary>
|
||||
public enum FalsifiabilityStatus
|
||||
{
|
||||
/// <summary>Status not determined</summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>Finding has been falsified (at least one criterion satisfied)</summary>
|
||||
Falsified,
|
||||
|
||||
/// <summary>Finding stands (all criteria not satisfied)</summary>
|
||||
NotFalsified,
|
||||
|
||||
/// <summary>Some criteria inconclusive</summary>
|
||||
PartiallyEvaluated
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Explainability.Assumptions;
|
||||
|
||||
namespace StellaOps.Scanner.Explainability.Falsifiability;
|
||||
|
||||
/// <summary>
|
||||
/// Input data for generating falsifiability criteria.
|
||||
/// </summary>
|
||||
public sealed record FalsifiabilityInput
|
||||
{
|
||||
/// <summary>The finding ID</summary>
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>The CVE or vulnerability ID</summary>
|
||||
public required string VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>Package name</summary>
|
||||
public required string PackageName { get; init; }
|
||||
|
||||
/// <summary>Installed version</summary>
|
||||
public required string InstalledVersion { get; init; }
|
||||
|
||||
/// <summary>Vulnerable version range</summary>
|
||||
public string? VulnerableRange { get; init; }
|
||||
|
||||
/// <summary>Fixed version, if available</summary>
|
||||
public string? FixedVersion { get; init; }
|
||||
|
||||
/// <summary>Assumptions made during analysis</summary>
|
||||
public AssumptionSet? Assumptions { get; init; }
|
||||
|
||||
/// <summary>Whether reachability analysis was performed</summary>
|
||||
public bool HasReachabilityData { get; init; }
|
||||
|
||||
/// <summary>Whether code is reachable (if analysis was performed)</summary>
|
||||
public bool? IsReachable { get; init; }
|
||||
|
||||
/// <summary>Known mitigations in place</summary>
|
||||
public ImmutableArray<string> Mitigations { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates falsifiability criteria for vulnerability findings.
|
||||
/// </summary>
|
||||
public interface IFalsifiabilityGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates falsifiability criteria for a finding.
|
||||
/// </summary>
|
||||
FalsifiabilityCriteria Generate(FalsifiabilityInput input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IFalsifiabilityGenerator"/>.
|
||||
/// </summary>
|
||||
public sealed class FalsifiabilityGenerator : IFalsifiabilityGenerator
|
||||
{
|
||||
private readonly ILogger<FalsifiabilityGenerator> _logger;
|
||||
|
||||
public FalsifiabilityGenerator(ILogger<FalsifiabilityGenerator> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public FalsifiabilityCriteria Generate(FalsifiabilityInput input)
|
||||
{
|
||||
_logger.LogDebug("Generating falsifiability criteria for finding {FindingId}", input.FindingId);
|
||||
|
||||
var criteria = new List<FalsificationCriterion>();
|
||||
|
||||
// Criterion 1: Package presence
|
||||
criteria.Add(new FalsificationCriterion(
|
||||
FalsificationType.PackageNotPresent,
|
||||
$"Package '{input.PackageName}' is not actually installed or is a false positive from manifest parsing",
|
||||
$"package.exists(\"{input.PackageName}\") == false",
|
||||
null,
|
||||
CriterionStatus.Pending));
|
||||
|
||||
// Criterion 2: Version mismatch
|
||||
if (input.VulnerableRange is not null)
|
||||
{
|
||||
criteria.Add(new FalsificationCriterion(
|
||||
FalsificationType.VersionMismatch,
|
||||
$"Installed version '{input.InstalledVersion}' is not within vulnerable range '{input.VulnerableRange}'",
|
||||
$"version.inRange(\"{input.InstalledVersion}\", \"{input.VulnerableRange}\") == false",
|
||||
null,
|
||||
CriterionStatus.Pending));
|
||||
}
|
||||
|
||||
// Criterion 3: Patch applied
|
||||
if (input.FixedVersion is not null)
|
||||
{
|
||||
criteria.Add(new FalsificationCriterion(
|
||||
FalsificationType.PatchApplied,
|
||||
$"Version '{input.InstalledVersion}' is at or above fixed version '{input.FixedVersion}'",
|
||||
$"version.gte(\"{input.InstalledVersion}\", \"{input.FixedVersion}\")",
|
||||
null,
|
||||
CriterionStatus.Pending));
|
||||
}
|
||||
|
||||
// Criterion 4: Code unreachable
|
||||
if (input.HasReachabilityData)
|
||||
{
|
||||
var reachabilityStatus = input.IsReachable switch
|
||||
{
|
||||
false => CriterionStatus.Satisfied,
|
||||
true => CriterionStatus.NotSatisfied,
|
||||
null => CriterionStatus.Inconclusive
|
||||
};
|
||||
|
||||
criteria.Add(new FalsificationCriterion(
|
||||
FalsificationType.CodeUnreachable,
|
||||
"Vulnerable code path is not reachable from application entry points",
|
||||
"reachability.isReachable() == false",
|
||||
input.IsReachable.HasValue ? $"Reachability analysis: {(input.IsReachable.Value ? "reachable" : "unreachable")}" : null,
|
||||
reachabilityStatus));
|
||||
}
|
||||
|
||||
// Criterion 5: Mitigations
|
||||
foreach (var mitigation in input.Mitigations)
|
||||
{
|
||||
criteria.Add(new FalsificationCriterion(
|
||||
FalsificationType.MitigationPresent,
|
||||
$"Mitigation '{mitigation}' prevents exploitation",
|
||||
null,
|
||||
$"Mitigation present: {mitigation}",
|
||||
CriterionStatus.Satisfied));
|
||||
}
|
||||
|
||||
// Criterion 6: Assumption-based criteria
|
||||
if (input.Assumptions is not null)
|
||||
{
|
||||
foreach (var assumption in input.Assumptions.Assumptions.Where(a => a.IsContradicted))
|
||||
{
|
||||
var type = assumption.Category switch
|
||||
{
|
||||
AssumptionCategory.NetworkExposure => FalsificationType.NoNetworkExposure,
|
||||
AssumptionCategory.ProcessPrivilege => FalsificationType.InsufficientPrivileges,
|
||||
AssumptionCategory.FeatureGate => FalsificationType.FeatureDisabled,
|
||||
AssumptionCategory.RuntimeConfig => FalsificationType.ConfigurationPrevents,
|
||||
AssumptionCategory.CompilerFlag => FalsificationType.MitigationPresent,
|
||||
_ => FalsificationType.RuntimePrevents
|
||||
};
|
||||
|
||||
criteria.Add(new FalsificationCriterion(
|
||||
type,
|
||||
$"Assumption '{assumption.Key}' was contradicted: assumed '{assumption.AssumedValue}', observed '{assumption.ObservedValue}'",
|
||||
null,
|
||||
$"Observed: {assumption.ObservedValue}",
|
||||
CriterionStatus.Satisfied));
|
||||
}
|
||||
}
|
||||
|
||||
// Determine overall status
|
||||
var status = DetermineOverallStatus(criteria);
|
||||
|
||||
// Generate summary
|
||||
var summary = GenerateSummary(input, criteria, status);
|
||||
|
||||
return new FalsifiabilityCriteria
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
FindingId = input.FindingId,
|
||||
Criteria = [.. criteria],
|
||||
Status = status,
|
||||
Summary = summary,
|
||||
GeneratedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private static FalsifiabilityStatus DetermineOverallStatus(List<FalsificationCriterion> criteria)
|
||||
{
|
||||
if (criteria.Count == 0)
|
||||
return FalsifiabilityStatus.Unknown;
|
||||
|
||||
if (criteria.Any(c => c.Status == CriterionStatus.Satisfied))
|
||||
return FalsifiabilityStatus.Falsified;
|
||||
|
||||
if (criteria.All(c => c.Status == CriterionStatus.NotSatisfied))
|
||||
return FalsifiabilityStatus.NotFalsified;
|
||||
|
||||
if (criteria.Any(c => c.Status is CriterionStatus.Pending or CriterionStatus.Inconclusive))
|
||||
return FalsifiabilityStatus.PartiallyEvaluated;
|
||||
|
||||
return FalsifiabilityStatus.Unknown;
|
||||
}
|
||||
|
||||
private static string GenerateSummary(
|
||||
FalsifiabilityInput input,
|
||||
List<FalsificationCriterion> criteria,
|
||||
FalsifiabilityStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
FalsifiabilityStatus.Falsified =>
|
||||
$"Finding {input.FindingId} can be falsified. " +
|
||||
$"Criteria satisfied: {string.Join(", ", criteria.Where(c => c.Status == CriterionStatus.Satisfied).Select(c => c.Type))}",
|
||||
|
||||
FalsifiabilityStatus.NotFalsified =>
|
||||
$"Finding {input.FindingId} has not been falsified. All {criteria.Count} criteria evaluated negative.",
|
||||
|
||||
FalsifiabilityStatus.PartiallyEvaluated =>
|
||||
$"Finding {input.FindingId} is partially evaluated. " +
|
||||
$"{criteria.Count(c => c.Status == CriterionStatus.Pending)} pending, " +
|
||||
$"{criteria.Count(c => c.Status == CriterionStatus.Inconclusive)} inconclusive.",
|
||||
|
||||
_ => $"Finding {input.FindingId} falsifiability status unknown."
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user