Add PHP Analyzer Plugin and Composer Lock Data Handling
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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:
@@ -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'");
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user