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

@@ -0,0 +1,152 @@
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using StellaOps.Findings.Ledger.Domain;
using StellaOps.Findings.Ledger.Infrastructure;
using StellaOps.Findings.Ledger.Infrastructure.AirGap;
using StellaOps.Findings.Ledger.Observability;
namespace StellaOps.Findings.Ledger.Services;
public sealed record AirgapImportInput(
string TenantId,
string BundleId,
string? MirrorGeneration,
string MerkleRoot,
DateTimeOffset TimeAnchor,
string? Publisher,
string? HashAlgorithm,
IReadOnlyList<string> Contents,
string? ImportOperator);
public sealed record AirgapImportResult(
bool Success,
Guid ChainId,
long? SequenceNumber,
Guid? LedgerEventId,
string? Error);
public sealed class AirgapImportService
{
private readonly ILedgerEventRepository _ledgerEventRepository;
private readonly ILedgerEventWriteService _writeService;
private readonly IAirgapImportRepository _repository;
private readonly TimeProvider _timeProvider;
private readonly ILogger<AirgapImportService> _logger;
public AirgapImportService(
ILedgerEventRepository ledgerEventRepository,
ILedgerEventWriteService writeService,
IAirgapImportRepository repository,
TimeProvider timeProvider,
ILogger<AirgapImportService> logger)
{
_ledgerEventRepository = ledgerEventRepository ?? throw new ArgumentNullException(nameof(ledgerEventRepository));
_writeService = writeService ?? throw new ArgumentNullException(nameof(writeService));
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<AirgapImportResult> RecordAsync(AirgapImportInput input, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(input);
var chainId = LedgerChainIdGenerator.FromTenantSubject(input.TenantId, $"airgap::{input.BundleId}");
var chainHead = await _ledgerEventRepository.GetChainHeadAsync(input.TenantId, chainId, cancellationToken).ConfigureAwait(false);
var sequence = (chainHead?.SequenceNumber ?? 0) + 1;
var previousHash = chainHead?.EventHash ?? LedgerEventConstants.EmptyHash;
var eventId = Guid.NewGuid();
var recordedAt = _timeProvider.GetUtcNow();
var payload = new JsonObject
{
["airgap"] = new JsonObject
{
["bundleId"] = input.BundleId,
["mirrorGeneration"] = input.MirrorGeneration,
["merkleRoot"] = input.MerkleRoot,
["timeAnchor"] = input.TimeAnchor.ToUniversalTime().ToString("O"),
["publisher"] = input.Publisher,
["hashAlgorithm"] = input.HashAlgorithm,
["contents"] = new JsonArray(input.Contents.Select(c => (JsonNode)c).ToArray())
}
};
var envelope = new JsonObject
{
["event"] = new JsonObject
{
["id"] = eventId.ToString(),
["type"] = LedgerEventConstants.EventAirgapBundleImported,
["tenant"] = input.TenantId,
["chainId"] = chainId.ToString(),
["sequence"] = sequence,
["policyVersion"] = input.MirrorGeneration ?? "airgap-bundle",
["artifactId"] = input.BundleId,
["finding"] = new JsonObject
{
["id"] = input.BundleId,
["artifactId"] = input.BundleId,
["vulnId"] = "airgap-import"
},
["actor"] = new JsonObject
{
["id"] = input.ImportOperator ?? "airgap-operator",
["type"] = "operator"
},
["occurredAt"] = FormatTimestamp(input.TimeAnchor),
["recordedAt"] = FormatTimestamp(recordedAt),
["payload"] = payload.DeepClone()
}
};
var draft = new LedgerEventDraft(
input.TenantId,
chainId,
sequence,
eventId,
LedgerEventConstants.EventAirgapBundleImported,
input.MirrorGeneration ?? "airgap-bundle",
input.BundleId,
input.BundleId,
SourceRunId: null,
ActorId: input.ImportOperator ?? "airgap-operator",
ActorType: "operator",
OccurredAt: input.TimeAnchor.ToUniversalTime(),
RecordedAt: recordedAt,
Payload: payload,
CanonicalEnvelope: envelope,
ProvidedPreviousHash: previousHash);
var writeResult = await _writeService.AppendAsync(draft, cancellationToken).ConfigureAwait(false);
if (writeResult.Status is not (LedgerWriteStatus.Success or LedgerWriteStatus.Idempotent))
{
var error = string.Join(";", writeResult.Errors);
return new AirgapImportResult(false, chainId, sequence, writeResult.Record?.EventId, error);
}
var ledgerEventId = writeResult.Record?.EventId;
var record = new AirgapImportRecord(
input.TenantId,
input.BundleId,
input.MirrorGeneration,
input.MerkleRoot,
input.TimeAnchor.ToUniversalTime(),
input.Publisher,
input.HashAlgorithm,
new JsonArray(input.Contents.Select(c => (JsonNode)c).ToArray()),
recordedAt,
input.ImportOperator,
ledgerEventId);
await _repository.InsertAsync(record, cancellationToken).ConfigureAwait(false);
LedgerTimeline.EmitAirgapImport(_logger, input.TenantId, input.BundleId, input.MerkleRoot, ledgerEventId);
return new AirgapImportResult(true, chainId, sequence, ledgerEventId, null);
}
private static string FormatTimestamp(DateTimeOffset value)
=> value.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'");
}

View File

@@ -22,6 +22,11 @@ public static class LedgerProjectionReducer
var status = evaluation.Status ?? DetermineStatus(record.EventType, payload, current?.Status);
var severity = evaluation.Severity ?? DetermineSeverity(payload, current?.Severity);
var riskScore = evaluation.RiskScore ?? current?.RiskScore;
var riskSeverity = evaluation.RiskSeverity ?? current?.RiskSeverity;
var riskProfileVersion = evaluation.RiskProfileVersion ?? current?.RiskProfileVersion;
var riskExplanationId = evaluation.RiskExplanationId ?? current?.RiskExplanationId;
var riskEventSequence = evaluation.RiskEventSequence ?? current?.RiskEventSequence ?? record.SequenceNumber;
var labels = CloneLabels(evaluation.Labels);
MergeLabels(labels, payload);
@@ -41,6 +46,11 @@ public static class LedgerProjectionReducer
record.PolicyVersion,
status,
severity,
riskScore,
riskSeverity,
riskProfileVersion,
riskExplanationId,
riskEventSequence,
labels,
record.EventId,
explainRef,

View File

@@ -0,0 +1,86 @@
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using StellaOps.Findings.Ledger.Hashing;
using StellaOps.Findings.Ledger.Infrastructure.Exports;
using StellaOps.Findings.Ledger.Observability;
namespace StellaOps.Findings.Ledger.Services;
public sealed record OrchestratorExportInput(
string TenantId,
Guid RunId,
string JobType,
string ArtifactHash,
string PolicyHash,
DateTimeOffset StartedAt,
DateTimeOffset? CompletedAt,
string Status,
string? ManifestPath,
string? LogsPath);
public sealed class OrchestratorExportService
{
private readonly IOrchestratorExportRepository _repository;
private readonly TimeProvider _timeProvider;
private readonly ILogger<OrchestratorExportService> _logger;
public OrchestratorExportService(
IOrchestratorExportRepository repository,
TimeProvider timeProvider,
ILogger<OrchestratorExportService> logger)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<OrchestratorExportRecord> RecordAsync(OrchestratorExportInput input, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(input);
var canonical = CreateCanonicalPayload(input);
var merkleRoot = HashUtilities.ComputeSha256Hex(LedgerCanonicalJsonSerializer.Serialize(canonical));
var record = new OrchestratorExportRecord(
input.TenantId,
input.RunId,
input.JobType,
input.ArtifactHash,
input.PolicyHash,
input.StartedAt.ToUniversalTime(),
input.CompletedAt?.ToUniversalTime(),
input.Status,
input.ManifestPath,
input.LogsPath,
merkleRoot,
_timeProvider.GetUtcNow());
await _repository.InsertAsync(record, cancellationToken).ConfigureAwait(false);
LedgerTimeline.EmitOrchestratorExport(_logger, record);
return record;
}
public Task<IReadOnlyList<OrchestratorExportRecord>> GetByArtifactAsync(string tenantId, string artifactHash, CancellationToken cancellationToken)
{
return _repository.GetByArtifactAsync(tenantId, artifactHash, cancellationToken);
}
private static JsonObject CreateCanonicalPayload(OrchestratorExportInput input)
{
var payload = new JsonObject
{
["tenantId"] = input.TenantId,
["runId"] = input.RunId.ToString(),
["jobType"] = input.JobType,
["artifactHash"] = input.ArtifactHash,
["policyHash"] = input.PolicyHash,
["startedAt"] = input.StartedAt.ToUniversalTime().ToString("O"),
["completedAt"] = input.CompletedAt?.ToUniversalTime().ToString("O"),
["status"] = input.Status,
["manifestPath"] = input.ManifestPath,
["logsPath"] = input.LogsPath
};
return LedgerCanonicalJsonSerializer.Canonicalize(payload);
}
}