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,108 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
namespace StellaOps.Scanner.Explainability.Assumptions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single assumption made during vulnerability analysis.
|
||||
/// Assumptions capture the conditions under which a finding is considered valid.
|
||||
/// </summary>
|
||||
/// <param name="Category">The category of assumption (compiler flag, runtime config, etc.)</param>
|
||||
/// <param name="Key">The specific assumption key (e.g., "-fstack-protector", "DEBUG_MODE")</param>
|
||||
/// <param name="AssumedValue">The value assumed during analysis</param>
|
||||
/// <param name="ObservedValue">The actual observed value, if available</param>
|
||||
/// <param name="Source">How the assumption was derived</param>
|
||||
/// <param name="Confidence">The confidence level in this assumption</param>
|
||||
public sealed record Assumption(
|
||||
AssumptionCategory Category,
|
||||
string Key,
|
||||
string AssumedValue,
|
||||
string? ObservedValue,
|
||||
AssumptionSource Source,
|
||||
ConfidenceLevel Confidence
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if the observed value matches the assumed value.
|
||||
/// </summary>
|
||||
public bool IsValidated => ObservedValue is not null &&
|
||||
string.Equals(AssumedValue, ObservedValue, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the observed value contradicts the assumed value.
|
||||
/// </summary>
|
||||
public bool IsContradicted => ObservedValue is not null &&
|
||||
!string.Equals(AssumedValue, ObservedValue, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Categories of assumptions that affect vulnerability exploitability.
|
||||
/// </summary>
|
||||
public enum AssumptionCategory
|
||||
{
|
||||
/// <summary>Compiler flags like -fstack-protector, -D_FORTIFY_SOURCE</summary>
|
||||
CompilerFlag,
|
||||
|
||||
/// <summary>Environment variables, config files, runtime settings</summary>
|
||||
RuntimeConfig,
|
||||
|
||||
/// <summary>Feature flags, build variants, conditional compilation</summary>
|
||||
FeatureGate,
|
||||
|
||||
/// <summary>LD_PRELOAD, RPATH, symbol versioning, loader behavior</summary>
|
||||
LoaderBehavior,
|
||||
|
||||
/// <summary>Port bindings, firewall rules, network exposure</summary>
|
||||
NetworkExposure,
|
||||
|
||||
/// <summary>Capabilities, seccomp profiles, AppArmor/SELinux policies</summary>
|
||||
ProcessPrivilege,
|
||||
|
||||
/// <summary>Memory layout assumptions (ASLR, PIE)</summary>
|
||||
MemoryProtection,
|
||||
|
||||
/// <summary>System call availability and filtering</summary>
|
||||
SyscallAvailability
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How an assumption was derived.
|
||||
/// </summary>
|
||||
public enum AssumptionSource
|
||||
{
|
||||
/// <summary>Default assumption when no evidence available</summary>
|
||||
Default,
|
||||
|
||||
/// <summary>Inferred from static analysis of binaries/code</summary>
|
||||
StaticAnalysis,
|
||||
|
||||
/// <summary>Observed from runtime telemetry</summary>
|
||||
RuntimeObservation,
|
||||
|
||||
/// <summary>Derived from container/image manifest</summary>
|
||||
ImageManifest,
|
||||
|
||||
/// <summary>Provided by user configuration</summary>
|
||||
UserProvided,
|
||||
|
||||
/// <summary>Extracted from Dockerfile or build configuration</summary>
|
||||
BuildConfig
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confidence level in an assumption.
|
||||
/// </summary>
|
||||
public enum ConfidenceLevel
|
||||
{
|
||||
/// <summary>No evidence, using defaults</summary>
|
||||
Low = 1,
|
||||
|
||||
/// <summary>Some indirect evidence</summary>
|
||||
Medium = 2,
|
||||
|
||||
/// <summary>Strong evidence from static analysis</summary>
|
||||
High = 3,
|
||||
|
||||
/// <summary>Direct runtime observation</summary>
|
||||
Verified = 4
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Explainability.Assumptions;
|
||||
|
||||
/// <summary>
|
||||
/// A collection of assumptions associated with a finding or analysis context.
|
||||
/// Provides methods for querying and validating assumptions.
|
||||
/// </summary>
|
||||
public sealed record AssumptionSet
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique identifier for this assumption set.
|
||||
/// </summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The assumptions in this set, keyed by category and key.
|
||||
/// </summary>
|
||||
public ImmutableArray<Assumption> Assumptions { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// When this assumption set was created.
|
||||
/// </summary>
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional context identifier (e.g., finding ID, image digest).
|
||||
/// </summary>
|
||||
public string? ContextId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all assumptions of a specific category.
|
||||
/// </summary>
|
||||
public IEnumerable<Assumption> GetByCategory(AssumptionCategory category) =>
|
||||
Assumptions.Where(a => a.Category == category);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a specific assumption by category and key.
|
||||
/// </summary>
|
||||
public Assumption? Get(AssumptionCategory category, string key) =>
|
||||
Assumptions.FirstOrDefault(a => a.Category == category &&
|
||||
string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// Returns the overall confidence level (minimum of all assumptions).
|
||||
/// </summary>
|
||||
public ConfidenceLevel OverallConfidence =>
|
||||
Assumptions.Length == 0
|
||||
? ConfidenceLevel.Low
|
||||
: Assumptions.Min(a => a.Confidence);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the count of validated assumptions.
|
||||
/// </summary>
|
||||
public int ValidatedCount => Assumptions.Count(a => a.IsValidated);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the count of contradicted assumptions.
|
||||
/// </summary>
|
||||
public int ContradictedCount => Assumptions.Count(a => a.IsContradicted);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if any assumption is contradicted by observed evidence.
|
||||
/// </summary>
|
||||
public bool HasContradictions => Assumptions.Any(a => a.IsContradicted);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the validation ratio (validated / total with observations).
|
||||
/// </summary>
|
||||
public double ValidationRatio
|
||||
{
|
||||
get
|
||||
{
|
||||
var withObservations = Assumptions.Count(a => a.ObservedValue is not null);
|
||||
return withObservations == 0 ? 0.0 : (double)ValidatedCount / withObservations;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new AssumptionSet with an additional assumption.
|
||||
/// </summary>
|
||||
public AssumptionSet WithAssumption(Assumption assumption) =>
|
||||
this with { Assumptions = Assumptions.Add(assumption) };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new AssumptionSet with updated observation for an assumption.
|
||||
/// </summary>
|
||||
public AssumptionSet WithObservation(AssumptionCategory category, string key, string observedValue)
|
||||
{
|
||||
var index = Assumptions.FindIndex(a =>
|
||||
a.Category == category &&
|
||||
string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (index < 0)
|
||||
return this;
|
||||
|
||||
var updated = Assumptions[index] with { ObservedValue = observedValue };
|
||||
return this with { Assumptions = Assumptions.SetItem(index, updated) };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for ImmutableArray to support FindIndex.
|
||||
/// </summary>
|
||||
internal static class ImmutableArrayExtensions
|
||||
{
|
||||
public static int FindIndex<T>(this ImmutableArray<T> array, Func<T, bool> predicate)
|
||||
{
|
||||
for (int i = 0; i < array.Length; i++)
|
||||
{
|
||||
if (predicate(array[i]))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
namespace StellaOps.Scanner.Explainability.Assumptions;
|
||||
|
||||
/// <summary>
|
||||
/// Collects assumptions from various sources during vulnerability analysis.
|
||||
/// </summary>
|
||||
public interface IAssumptionCollector
|
||||
{
|
||||
/// <summary>
|
||||
/// Records an assumption made during analysis.
|
||||
/// </summary>
|
||||
/// <param name="category">The category of assumption</param>
|
||||
/// <param name="key">The assumption key</param>
|
||||
/// <param name="assumedValue">The assumed value</param>
|
||||
/// <param name="source">How the assumption was derived</param>
|
||||
/// <param name="confidence">Confidence level</param>
|
||||
void Record(
|
||||
AssumptionCategory category,
|
||||
string key,
|
||||
string assumedValue,
|
||||
AssumptionSource source,
|
||||
ConfidenceLevel confidence = ConfidenceLevel.Low);
|
||||
|
||||
/// <summary>
|
||||
/// Records an observation that validates or contradicts an assumption.
|
||||
/// </summary>
|
||||
/// <param name="category">The category of assumption</param>
|
||||
/// <param name="key">The assumption key</param>
|
||||
/// <param name="observedValue">The observed value</param>
|
||||
void RecordObservation(AssumptionCategory category, string key, string observedValue);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the final assumption set from collected assumptions.
|
||||
/// </summary>
|
||||
/// <param name="contextId">Optional context identifier</param>
|
||||
/// <returns>The completed assumption set</returns>
|
||||
AssumptionSet Build(string? contextId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Clears all collected assumptions.
|
||||
/// </summary>
|
||||
void Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IAssumptionCollector"/>.
|
||||
/// </summary>
|
||||
public sealed class AssumptionCollector : IAssumptionCollector
|
||||
{
|
||||
private readonly Dictionary<(AssumptionCategory, string), Assumption> _assumptions = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Record(
|
||||
AssumptionCategory category,
|
||||
string key,
|
||||
string assumedValue,
|
||||
AssumptionSource source,
|
||||
ConfidenceLevel confidence = ConfidenceLevel.Low)
|
||||
{
|
||||
var normalizedKey = key.ToLowerInvariant();
|
||||
var existing = _assumptions.GetValueOrDefault((category, normalizedKey));
|
||||
|
||||
// Keep assumption with higher confidence
|
||||
if (existing is null || confidence > existing.Confidence)
|
||||
{
|
||||
_assumptions[(category, normalizedKey)] = new Assumption(
|
||||
category,
|
||||
key,
|
||||
assumedValue,
|
||||
existing?.ObservedValue,
|
||||
source,
|
||||
confidence);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RecordObservation(AssumptionCategory category, string key, string observedValue)
|
||||
{
|
||||
var normalizedKey = key.ToLowerInvariant();
|
||||
if (_assumptions.TryGetValue((category, normalizedKey), out var existing))
|
||||
{
|
||||
_assumptions[(category, normalizedKey)] = existing with
|
||||
{
|
||||
ObservedValue = observedValue,
|
||||
Confidence = ConfidenceLevel.Verified
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Record observation even without prior assumption
|
||||
_assumptions[(category, normalizedKey)] = new Assumption(
|
||||
category,
|
||||
key,
|
||||
observedValue, // Use observed as assumed when no prior assumption
|
||||
observedValue,
|
||||
AssumptionSource.RuntimeObservation,
|
||||
ConfidenceLevel.Verified);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public AssumptionSet Build(string? contextId = null)
|
||||
{
|
||||
return new AssumptionSet
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Assumptions = [.. _assumptions.Values],
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
ContextId = contextId
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Clear() => _assumptions.Clear();
|
||||
}
|
||||
Reference in New Issue
Block a user