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

@@ -1155,72 +1155,29 @@ internal static class CommandFactory
var advise = new Command("advise", "Interact with Advisory AI pipelines.");
_ = options;
var run = new Command("run", "Generate Advisory AI output for the specified task.");
var taskArgument = new Argument<string>("task")
var runOptions = CreateAdvisoryOptions();
var runTaskArgument = new Argument<string>("task")
{
Description = "Task to run (summary, conflict, remediation)."
};
run.Add(taskArgument);
var advisoryKeyOption = new Option<string>("--advisory-key")
{
Description = "Advisory identifier to summarise (required).",
Required = true
};
var artifactIdOption = new Option<string?>("--artifact-id")
{
Description = "Optional artifact identifier to scope SBOM context."
};
var artifactPurlOption = new Option<string?>("--artifact-purl")
{
Description = "Optional package URL to scope dependency context."
};
var policyVersionOption = new Option<string?>("--policy-version")
{
Description = "Policy revision to evaluate (defaults to current)."
};
var profileOption = new Option<string?>("--profile")
{
Description = "Advisory AI execution profile (default, fips-local, etc.)."
};
var sectionOption = new Option<string[]>("--section")
{
Description = "Preferred context sections to emphasise (repeatable).",
Arity = ArgumentArity.ZeroOrMore
};
sectionOption.AllowMultipleArgumentsPerToken = true;
var forceRefreshOption = new Option<bool>("--force-refresh")
{
Description = "Bypass cached plan/output and recompute."
};
var timeoutOption = new Option<int?>("--timeout")
{
Description = "Seconds to wait for generated output before timing out (0 = single attempt)."
};
timeoutOption.Arity = ArgumentArity.ZeroOrOne;
run.Add(advisoryKeyOption);
run.Add(artifactIdOption);
run.Add(artifactPurlOption);
run.Add(policyVersionOption);
run.Add(profileOption);
run.Add(sectionOption);
run.Add(forceRefreshOption);
run.Add(timeoutOption);
var run = new Command("run", "Generate Advisory AI output for the specified task.");
run.Add(runTaskArgument);
AddAdvisoryOptions(run, runOptions);
run.SetAction((parseResult, _) =>
{
var taskValue = parseResult.GetValue(taskArgument);
var advisoryKey = parseResult.GetValue(advisoryKeyOption) ?? string.Empty;
var artifactId = parseResult.GetValue(artifactIdOption);
var artifactPurl = parseResult.GetValue(artifactPurlOption);
var policyVersion = parseResult.GetValue(policyVersionOption);
var profile = parseResult.GetValue(profileOption) ?? "default";
var sections = parseResult.GetValue(sectionOption) ?? Array.Empty<string>();
var forceRefresh = parseResult.GetValue(forceRefreshOption);
var timeoutSeconds = parseResult.GetValue(timeoutOption) ?? 120;
var taskValue = parseResult.GetValue(runTaskArgument);
var advisoryKey = parseResult.GetValue(runOptions.AdvisoryKey) ?? string.Empty;
var artifactId = parseResult.GetValue(runOptions.ArtifactId);
var artifactPurl = parseResult.GetValue(runOptions.ArtifactPurl);
var policyVersion = parseResult.GetValue(runOptions.PolicyVersion);
var profile = parseResult.GetValue(runOptions.Profile) ?? "default";
var sections = parseResult.GetValue(runOptions.Sections) ?? Array.Empty<string>();
var forceRefresh = parseResult.GetValue(runOptions.ForceRefresh);
var timeoutSeconds = parseResult.GetValue(runOptions.TimeoutSeconds) ?? 120;
var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(runOptions.Format));
var outputPath = parseResult.GetValue(runOptions.Output);
var verbose = parseResult.GetValue(verboseOption);
if (!Enum.TryParse<AdvisoryAiTaskType>(taskValue, ignoreCase: true, out var taskType))
@@ -1239,17 +1196,164 @@ internal static class CommandFactory
sections,
forceRefresh,
timeoutSeconds,
outputFormat,
outputPath,
verbose,
cancellationToken);
});
var summarizeOptions = CreateAdvisoryOptions();
var summarize = new Command("summarize", "Summarize an advisory with JSON/Markdown outputs and citations.");
AddAdvisoryOptions(summarize, summarizeOptions);
summarize.SetAction((parseResult, _) =>
{
var advisoryKey = parseResult.GetValue(summarizeOptions.AdvisoryKey) ?? string.Empty;
var artifactId = parseResult.GetValue(summarizeOptions.ArtifactId);
var artifactPurl = parseResult.GetValue(summarizeOptions.ArtifactPurl);
var policyVersion = parseResult.GetValue(summarizeOptions.PolicyVersion);
var profile = parseResult.GetValue(summarizeOptions.Profile) ?? "default";
var sections = parseResult.GetValue(summarizeOptions.Sections) ?? Array.Empty<string>();
var forceRefresh = parseResult.GetValue(summarizeOptions.ForceRefresh);
var timeoutSeconds = parseResult.GetValue(summarizeOptions.TimeoutSeconds) ?? 120;
var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(summarizeOptions.Format));
var outputPath = parseResult.GetValue(summarizeOptions.Output);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleAdviseRunAsync(
services,
AdvisoryAiTaskType.Summary,
advisoryKey,
artifactId,
artifactPurl,
policyVersion,
profile,
sections,
forceRefresh,
timeoutSeconds,
outputFormat,
outputPath,
verbose,
cancellationToken);
});
advise.Add(run);
advise.Add(summarize);
return advise;
}
private static AdvisoryCommandOptions CreateAdvisoryOptions()
{
var advisoryKey = new Option<string>("--advisory-key")
{
Description = "Advisory identifier to summarise (required).",
Required = true
};
var artifactId = new Option<string?>("--artifact-id")
{
Description = "Optional artifact identifier to scope SBOM context."
};
var artifactPurl = new Option<string?>("--artifact-purl")
{
Description = "Optional package URL to scope dependency context."
};
var policyVersion = new Option<string?>("--policy-version")
{
Description = "Policy revision to evaluate (defaults to current)."
};
var profile = new Option<string?>("--profile")
{
Description = "Advisory AI execution profile (default, fips-local, etc.)."
};
var sections = new Option<string[]>("--section")
{
Description = "Preferred context sections to emphasise (repeatable).",
Arity = ArgumentArity.ZeroOrMore
};
sections.AllowMultipleArgumentsPerToken = true;
var forceRefresh = new Option<bool>("--force-refresh")
{
Description = "Bypass cached plan/output and recompute."
};
var timeoutSeconds = new Option<int?>("--timeout")
{
Description = "Seconds to wait for generated output before timing out (0 = single attempt)."
};
timeoutSeconds.Arity = ArgumentArity.ZeroOrOne;
var format = new Option<string?>("--format")
{
Description = "Output format: table (default), json, or markdown."
};
var output = new Option<string?>("--output")
{
Description = "File path to write advisory output when using json/markdown formats."
};
return new AdvisoryCommandOptions(
advisoryKey,
artifactId,
artifactPurl,
policyVersion,
profile,
sections,
forceRefresh,
timeoutSeconds,
format,
output);
}
private static void AddAdvisoryOptions(Command command, AdvisoryCommandOptions options)
{
command.Add(options.AdvisoryKey);
command.Add(options.ArtifactId);
command.Add(options.ArtifactPurl);
command.Add(options.PolicyVersion);
command.Add(options.Profile);
command.Add(options.Sections);
command.Add(options.ForceRefresh);
command.Add(options.TimeoutSeconds);
command.Add(options.Format);
command.Add(options.Output);
}
private static AdvisoryOutputFormat ParseAdvisoryOutputFormat(string? formatValue)
{
var normalized = string.IsNullOrWhiteSpace(formatValue)
? "table"
: formatValue!.Trim().ToLowerInvariant();
return normalized switch
{
"json" => AdvisoryOutputFormat.Json,
"markdown" => AdvisoryOutputFormat.Markdown,
"md" => AdvisoryOutputFormat.Markdown,
_ => AdvisoryOutputFormat.Table
};
}
private sealed record AdvisoryCommandOptions(
Option<string> AdvisoryKey,
Option<string?> ArtifactId,
Option<string?> ArtifactPurl,
Option<string?> PolicyVersion,
Option<string?> Profile,
Option<string[]> Sections,
Option<bool> ForceRefresh,
Option<int?> TimeoutSeconds,
Option<string?> Format,
Option<string?> Output);
private static Command BuildVulnCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
{
var vuln = new Command("vuln", "Explore vulnerability observations and overlays.");
{
var vuln = new Command("vuln", "Explore vulnerability observations and overlays.");
var observations = new Command("observations", "List raw advisory observations for overlay consumers.");

View File

@@ -448,6 +448,8 @@ internal static class CommandHandlers
IReadOnlyList<string> preferredSections,
bool forceRefresh,
int timeoutSeconds,
AdvisoryOutputFormat outputFormat,
string? outputPath,
bool verbose,
CancellationToken cancellationToken)
{
@@ -542,7 +544,14 @@ internal static class CommandHandlers
activity?.SetTag("stellaops.cli.advisory.cache_hit", output.PlanFromCache);
logger.LogInformation("Advisory output ready (cache key {CacheKey}).", output.CacheKey);
RenderAdvisoryOutput(output);
var rendered = RenderAdvisoryOutput(output, outputFormat);
if (!string.IsNullOrWhiteSpace(outputPath) && rendered is not null)
{
var fullPath = Path.GetFullPath(outputPath!);
await File.WriteAllTextAsync(fullPath, rendered, cancellationToken).ConfigureAwait(false);
logger.LogInformation("Advisory output written to {Path}.", fullPath);
}
if (output.Guardrail.Blocked)
{
@@ -6326,7 +6335,113 @@ internal static class CommandHandlers
}
}
private static void RenderAdvisoryOutput(AdvisoryPipelineOutputModel output)
private static string? RenderAdvisoryOutput(AdvisoryPipelineOutputModel output, AdvisoryOutputFormat format)
{
return format switch
{
AdvisoryOutputFormat.Json => RenderAdvisoryOutputJson(output),
AdvisoryOutputFormat.Markdown => RenderAdvisoryOutputMarkdown(output),
_ => RenderAdvisoryOutputTable(output)
};
}
private static string RenderAdvisoryOutputJson(AdvisoryPipelineOutputModel output)
{
return JsonSerializer.Serialize(output, new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = true
});
}
private static string RenderAdvisoryOutputMarkdown(AdvisoryPipelineOutputModel output)
{
var builder = new StringBuilder();
builder.AppendLine($"# Advisory {output.TaskType} ({output.Profile})");
builder.AppendLine();
builder.AppendLine($"- Cache Key: `{output.CacheKey}`");
builder.AppendLine($"- Generated: {output.GeneratedAtUtc.ToString(\"O\", CultureInfo.InvariantCulture)}");
builder.AppendLine($"- Plan From Cache: {(output.PlanFromCache ? \"yes\" : \"no\")}");
builder.AppendLine($"- Guardrail Blocked: {(output.Guardrail.Blocked ? \"yes\" : \"no\")}");
builder.AppendLine();
if (!string.IsNullOrWhiteSpace(output.Response))
{
builder.AppendLine("## Response");
builder.AppendLine(output.Response.Trim());
builder.AppendLine();
}
if (!string.IsNullOrWhiteSpace(output.Prompt))
{
builder.AppendLine("## Prompt (sanitized)");
builder.AppendLine(output.Prompt.Trim());
builder.AppendLine();
}
if (output.Citations.Count > 0)
{
builder.AppendLine("## Citations");
foreach (var citation in output.Citations.OrderBy(c => c.Index))
{
builder.AppendLine($"- [{citation.Index}] {citation.DocumentId} :: {citation.ChunkId}");
}
builder.AppendLine();
}
if (output.Metadata.Count > 0)
{
builder.AppendLine("## Output Metadata");
foreach (var entry in output.Metadata.OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase))
{
builder.AppendLine($"- **{entry.Key}**: {entry.Value}");
}
builder.AppendLine();
}
if (output.Guardrail.Metadata.Count > 0)
{
builder.AppendLine("## Guardrail Metadata");
foreach (var entry in output.Guardrail.Metadata.OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase))
{
builder.AppendLine($"- **{entry.Key}**: {entry.Value}");
}
builder.AppendLine();
}
if (output.Guardrail.Violations.Count > 0)
{
builder.AppendLine("## Guardrail Violations");
foreach (var violation in output.Guardrail.Violations)
{
builder.AppendLine($"- `{violation.Code}`: {violation.Message}");
}
builder.AppendLine();
}
builder.AppendLine("## Provenance");
builder.AppendLine($"- Input Digest: `{output.Provenance.InputDigest}`");
builder.AppendLine($"- Output Hash: `{output.Provenance.OutputHash}`");
if (output.Provenance.Signatures.Count > 0)
{
foreach (var signature in output.Provenance.Signatures)
{
builder.AppendLine($"- Signature: `{signature}`");
}
}
else
{
builder.AppendLine("- Signature: none");
}
return builder.ToString();
}
private static string? RenderAdvisoryOutputTable(AdvisoryPipelineOutputModel output)
{
var console = AnsiConsole.Console;
@@ -6428,6 +6543,8 @@ internal static class CommandHandlers
provenance.AddRow("Signatures", signatures);
console.Write(provenance);
return null;
}
private static Table CreateKeyValueTable(string title, IReadOnlyDictionary<string, string> entries)