Add PHP Analyzer Plugin and Composer Lock Data Handling
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Implemented the PhpAnalyzerPlugin to analyze PHP projects.
- Created ComposerLockData class to represent data from composer.lock files.
- Developed ComposerLockReader to load and parse composer.lock files asynchronously.
- Introduced ComposerPackage class to encapsulate package details.
- Added PhpPackage class to represent PHP packages with metadata and evidence.
- Implemented PhpPackageCollector to gather packages from ComposerLockData.
- Created PhpLanguageAnalyzer to perform analysis and emit results.
- Added capability signals for known PHP frameworks and CMS.
- Developed unit tests for the PHP language analyzer and its components.
- Included sample composer.lock and expected output for testing.
- Updated project files for the new PHP analyzer library and tests.
This commit is contained in:
StellaOps Bot
2025-11-22 14:02:49 +02:00
parent a7f3c7869a
commit b6b9ffc050
158 changed files with 16272 additions and 809 deletions

View File

@@ -189,6 +189,7 @@ internal static class AdvisoryLinksetNormalization
var reason = key switch
{
"severity" => "severity-mismatch",
var k when k.StartsWith("cvss", StringComparison.OrdinalIgnoreCase) => "cvss-mismatch",
"ranges" => "affected-range-divergence",
"references" => "reference-clash",
"aliases" => "alias-inconsistency",

View File

@@ -4,6 +4,8 @@ using System.Collections.Immutable;
using System.Linq;
using StellaOps.Concelier.Models;
#pragma warning disable CS8620 // nullability mismatches guarded by explicit filtering
namespace StellaOps.Concelier.Core.Linksets;
internal static class LinksetCorrelation
@@ -109,19 +111,15 @@ internal static class LinksetCorrelation
List<HashSet<string>> packageKeysPerInput = inputs
.Select(i => i.Purls
.Select(ExtractPackageKey)
.Where(k => !string.IsNullOrEmpty(k))
.Where(k => !string.IsNullOrWhiteSpace(k))
.ToHashSet(StringComparer.Ordinal))
.ToList();
var sharedPackages = packageKeysPerInput
.Skip(1)
.Aggregate(
new HashSet<string>(packageKeysPerInput.First()!, StringComparer.Ordinal),
(acc, next) =>
{
acc.IntersectWith(next!);
return acc;
});
var sharedPackages = new HashSet<string>(packageKeysPerInput.FirstOrDefault() ?? new HashSet<string>(), StringComparer.Ordinal);
foreach (var next in packageKeysPerInput.Skip(1))
{
sharedPackages.IntersectWith(next);
}
if (sharedPackages.Count > 0)
{
@@ -140,12 +138,17 @@ internal static class LinksetCorrelation
private static IEnumerable<AdvisoryLinksetConflict> CollectRangeConflicts(
IReadOnlyCollection<Input> inputs,
HashSet<string> sharedPackages)
HashSet<string?> sharedPackages)
{
var conflicts = new List<AdvisoryLinksetConflict>();
foreach (var package in sharedPackages)
{
if (package is null)
{
continue;
}
var values = inputs
.SelectMany(i => i.Purls
.Where(p => ExtractPackageKey(p) == package)
@@ -169,6 +172,8 @@ internal static class LinksetCorrelation
return conflicts;
}
#pragma warning restore CS8620
private static bool HasExactPurlOverlap(IReadOnlyCollection<Input> inputs)
{
var first = inputs.First().Purls.ToHashSet(StringComparer.Ordinal);

View File

@@ -28,4 +28,20 @@ public sealed class AdvisoryLinksetNormalizationConfidenceTests
Assert.Equal("severity-mismatch", conflict.Reason);
Assert.Contains("severity:mismatch", conflict.Values!);
}
[Fact]
public void FromRawLinksetWithConfidence_EmitsCvssMismatchConflict()
{
var linkset = new RawLinkset
{
PackageUrls = ImmutableArray.Create("pkg:maven/com.acme/foo@2.0.0"),
Notes = ImmutableDictionary.CreateRange(new[] { new KeyValuePair<string, string>("cvss_v3", "7.5/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H") })
};
var (_, _, conflicts) = AdvisoryLinksetNormalization.FromRawLinksetWithConfidence(linkset);
var conflict = Assert.Single(conflicts);
Assert.Equal("cvss-mismatch", conflict.Reason);
Assert.Contains("cvss_v3:7.5/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", conflict.Values!);
}
}

View File

@@ -96,6 +96,7 @@ public sealed class AdvisoryObservationAggregationTests
Assert.Contains(aggregate.Conflicts, c => c.Reason == "alias-inconsistency");
Assert.Contains(aggregate.Conflicts, c => c.Reason == "affected-range-divergence");
Assert.True(aggregate.Confidence is > 0.0 and < 1.0);
Assert.All(aggregate.Conflicts, c => Assert.NotNull(c.SourceIds));
}
[Fact]

View File

@@ -33,8 +33,8 @@ public class AdvisoryObservationTransportWorkerTests
"hash-1",
DateTimeOffset.UtcNow,
ReplayCursor: "cursor-1",
supersedesId: null,
traceId: "trace-1");
SupersedesId: null,
TraceId: "trace-1");
var outbox = new FakeOutbox(evt);
var transport = new FakeTransport();