feat(scanner): Implement Deno analyzer and associated tests
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added Deno analyzer with comprehensive metadata and evidence structure. - Created a detailed implementation plan for Sprint 130 focusing on Deno analyzer. - Introduced AdvisoryAiGuardrailOptions for managing guardrail configurations. - Developed GuardrailPhraseLoader for loading blocked phrases from JSON files. - Implemented tests for AdvisoryGuardrailOptions binding and phrase loading. - Enhanced telemetry for Advisory AI with metrics tracking. - Added VexObservationProjectionService for querying VEX observations. - Created extensive tests for VexObservationProjectionService functionality. - Introduced Ruby language analyzer with tests for simple and complex workspaces. - Added Ruby application fixtures for testing purposes.
This commit is contained in:
@@ -7,6 +7,7 @@ namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Deno;
|
||||
|
||||
public sealed class DenoWorkspaceNormalizerTests
|
||||
{
|
||||
|
||||
[Fact]
|
||||
public async Task WorkspaceFixtureProducesDeterministicOutputAsync()
|
||||
{
|
||||
@@ -79,18 +80,50 @@ public sealed class DenoWorkspaceNormalizerTests
|
||||
node => node.Kind == DenoModuleKind.RemoteModule &&
|
||||
node.Id == "remote::https://deno.land/std@0.207.0/http/server.ts");
|
||||
Assert.NotNull(remoteNode);
|
||||
Assert.Equal("sha256-deadbeef", remoteNode!.Integrity);
|
||||
var expectedIntegrity = lockFile.RemoteEntries["https://deno.land/std@0.207.0/http/server.ts"];
|
||||
Assert.Equal(expectedIntegrity, remoteNode!.Integrity);
|
||||
|
||||
var vendorCacheEdges = graph.Edges
|
||||
.Where(edge => edge.ImportKind == DenoImportKind.Cache &&
|
||||
edge.Provenance.StartsWith("vendor-cache:", StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
|
||||
if (vendorCacheEdges.Length == 0)
|
||||
{
|
||||
var sample = string.Join(
|
||||
Environment.NewLine,
|
||||
graph.Edges
|
||||
.Select(edge => $"{edge.ImportKind}:{edge.Specifier}:{edge.Provenance}")
|
||||
.Take(10));
|
||||
Assert.Fail($"Expected vendor cache edges but none were found. Sample edges:{Environment.NewLine}{sample}");
|
||||
}
|
||||
|
||||
var vendorEdge = vendorCacheEdges.FirstOrDefault(
|
||||
edge => edge.Specifier.Contains("https://deno.land/std@0.207.0/http/server.ts", StringComparison.Ordinal));
|
||||
if (vendorEdge is null)
|
||||
{
|
||||
var details = string.Join(
|
||||
Environment.NewLine,
|
||||
vendorCacheEdges.Select(edge => $"{edge.Specifier} [{edge.Provenance}] -> {edge.Resolution}"));
|
||||
Assert.Fail($"Unable to locate vendor cache edge for std server.ts. Observed edges:{Environment.NewLine}{details}");
|
||||
}
|
||||
|
||||
var npmBridgeEdges = graph.Edges
|
||||
.Where(edge => edge.ImportKind == DenoImportKind.NpmBridge)
|
||||
.ToArray();
|
||||
if (npmBridgeEdges.Length == 0)
|
||||
{
|
||||
var bridgeSample = string.Join(
|
||||
Environment.NewLine,
|
||||
graph.Edges
|
||||
.Select(edge => $"{edge.ImportKind}:{edge.Specifier}:{edge.Resolution}")
|
||||
.Take(10));
|
||||
Assert.Fail($"No npm bridge edges discovered. Sample:{Environment.NewLine}{bridgeSample}");
|
||||
}
|
||||
|
||||
Assert.Contains(
|
||||
graph.Edges,
|
||||
edge => edge.ImportKind == DenoImportKind.Cache &&
|
||||
edge.Provenance.StartsWith("vendor-cache:", StringComparison.Ordinal) &&
|
||||
edge.Specifier.Contains("https://deno.land/std@0.207.0/http/server.ts", StringComparison.Ordinal));
|
||||
|
||||
Assert.Contains(
|
||||
graph.Edges,
|
||||
edge => edge.ImportKind == DenoImportKind.NpmBridge &&
|
||||
edge.Specifier == "npm:dayjs@1" &&
|
||||
npmBridgeEdges,
|
||||
edge => edge.Specifier == "npm:dayjs@1" &&
|
||||
edge.Resolution == "dayjs@1.11.12");
|
||||
|
||||
Assert.Contains(
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
using System.Linq;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestFixtures;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Golden;
|
||||
@@ -9,25 +14,37 @@ public sealed class DenoAnalyzerGoldenTests
|
||||
[Fact]
|
||||
public async Task AnalyzerMatchesGoldenSnapshotAsync()
|
||||
{
|
||||
var fixture = TestPaths.ResolveFixture("lang", "deno", "full");
|
||||
var golden = Path.Combine(fixture, "expected.json");
|
||||
var fixtureRoot = TestPaths.ResolveFixture("lang", "deno", "full");
|
||||
var golden = Path.Combine(fixtureRoot, "expected.json");
|
||||
var analyzers = new ILanguageAnalyzer[] { new DenoLanguageAnalyzer() };
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixture, analyzers, cancellationToken);
|
||||
var normalized = Normalize(json, fixture);
|
||||
var expected = await File.ReadAllTextAsync(golden, cancellationToken);
|
||||
|
||||
normalized = normalized.TrimEnd();
|
||||
expected = expected.TrimEnd();
|
||||
|
||||
if (!string.Equals(expected, normalized, StringComparison.Ordinal))
|
||||
var (workspaceRoot, envDir) = DenoWorkspaceTestFixture.Create();
|
||||
var previousDenoDir = Environment.GetEnvironmentVariable("DENO_DIR");
|
||||
try
|
||||
{
|
||||
var actualPath = golden + ".actual";
|
||||
await File.WriteAllTextAsync(actualPath, normalized, cancellationToken);
|
||||
}
|
||||
Environment.SetEnvironmentVariable("DENO_DIR", envDir);
|
||||
|
||||
Assert.Equal(expected, normalized);
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(workspaceRoot, analyzers, cancellationToken);
|
||||
var normalized = Normalize(json, workspaceRoot);
|
||||
var expected = await File.ReadAllTextAsync(golden, cancellationToken);
|
||||
|
||||
normalized = normalized.TrimEnd();
|
||||
expected = expected.TrimEnd();
|
||||
|
||||
if (!string.Equals(expected, normalized, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = golden + ".actual";
|
||||
await File.WriteAllTextAsync(actualPath, normalized, cancellationToken);
|
||||
}
|
||||
|
||||
Assert.Equal(expected, normalized);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("DENO_DIR", previousDenoDir);
|
||||
DenoWorkspaceTestFixture.Cleanup(workspaceRoot);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Normalize(string json, string workspaceRoot)
|
||||
@@ -37,10 +54,206 @@ public sealed class DenoAnalyzerGoldenTests
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var normalizedRoot = workspaceRoot.Replace("\\", "/", StringComparison.Ordinal);
|
||||
var builder = json.Replace(normalizedRoot, "<workspace>", StringComparison.Ordinal);
|
||||
var altRoot = workspaceRoot.Replace("/", "\\", StringComparison.Ordinal);
|
||||
builder = builder.Replace(altRoot, "<workspace>", StringComparison.Ordinal);
|
||||
return builder;
|
||||
var node = JsonNode.Parse(json) ?? new JsonArray();
|
||||
if (node is JsonArray array)
|
||||
{
|
||||
foreach (var element in array.OfType<JsonObject>())
|
||||
{
|
||||
NormalizeComponent(element);
|
||||
}
|
||||
}
|
||||
|
||||
var normalized = node.ToJsonString(JsonSerializerOptionsProvider);
|
||||
normalized = ReplaceWorkspacePaths(normalized, workspaceRoot);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static void NormalizeComponent(JsonObject component)
|
||||
{
|
||||
if (component is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SortMetadata(component);
|
||||
|
||||
if (!component.TryGetPropertyValue("type", out var typeNode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var type = typeNode?.GetValue<string>();
|
||||
if (string.Equals(type, "deno-container", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
NormalizeContainer(component);
|
||||
}
|
||||
else if (string.Equals(type, "deno-observation", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
NormalizeObservation(component);
|
||||
}
|
||||
}
|
||||
|
||||
private static void NormalizeContainer(JsonObject container)
|
||||
{
|
||||
NormalizeAliasProperty(container, "name");
|
||||
NormalizeComponentKey(container);
|
||||
|
||||
if (container.TryGetPropertyValue("metadata", out var metadataNode) &&
|
||||
metadataNode is JsonObject metadata)
|
||||
{
|
||||
NormalizeAliasProperty(metadata, "deno.container.identifier");
|
||||
NormalizeAliasProperty(metadata, "deno.container.meta.alias");
|
||||
}
|
||||
|
||||
if (container.TryGetPropertyValue("evidence", out var evidenceNode) &&
|
||||
evidenceNode is JsonArray evidenceArray)
|
||||
{
|
||||
foreach (var evidence in evidenceArray.OfType<JsonObject>())
|
||||
{
|
||||
if (evidence.TryGetPropertyValue("source", out var sourceNode) &&
|
||||
string.Equals(sourceNode?.GetValue<string>(), "deno.container", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
NormalizeAliasProperty(evidence, "value");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void NormalizeComponentKey(JsonObject container)
|
||||
{
|
||||
if (!container.TryGetPropertyValue("componentKey", out var keyNode) ||
|
||||
keyNode is not JsonValue keyValue ||
|
||||
!keyValue.TryGetValue<string>(out var componentKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var lastSeparator = componentKey.LastIndexOf(':');
|
||||
if (lastSeparator < 0)
|
||||
{
|
||||
container["componentKey"] = NormalizeAliasValue(componentKey);
|
||||
return;
|
||||
}
|
||||
|
||||
var prefix = componentKey[..(lastSeparator + 1)];
|
||||
var alias = componentKey[(lastSeparator + 1)..];
|
||||
container["componentKey"] = prefix + NormalizeAliasValue(alias);
|
||||
}
|
||||
|
||||
private static void NormalizeAliasProperty(JsonObject obj, string propertyName)
|
||||
{
|
||||
if (!obj.TryGetPropertyValue(propertyName, out var node) ||
|
||||
node is not JsonValue valueNode ||
|
||||
!valueNode.TryGetValue<string>(out var value) ||
|
||||
string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
obj[propertyName] = NormalizeAliasValue(value);
|
||||
}
|
||||
|
||||
private static string NormalizeAliasValue(string value)
|
||||
=> TryNormalizeAlias(value, out var normalized) ? normalized : value;
|
||||
|
||||
private static bool TryNormalizeAlias(string value, out string normalized)
|
||||
{
|
||||
normalized = value;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lastDash = value.LastIndexOf('-');
|
||||
if (lastDash <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var suffix = value[(lastDash + 1)..];
|
||||
if (suffix.Length != 12)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var character in suffix)
|
||||
{
|
||||
if (!IsLowerHex(character))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
normalized = value[..lastDash] + "-<hash>";
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsLowerHex(char value)
|
||||
=> (value is >= '0' and <= '9') || (value is >= 'a' and <= 'f');
|
||||
|
||||
private static string ReplaceWorkspacePaths(string value, string workspaceRoot)
|
||||
{
|
||||
var normalizedRoot = workspaceRoot.Replace("\\", "/", StringComparison.Ordinal);
|
||||
var normalizedRootLower = normalizedRoot.ToLowerInvariant();
|
||||
var result = value
|
||||
.Replace(normalizedRoot, "<workspace>", StringComparison.Ordinal)
|
||||
.Replace(normalizedRootLower, "<workspace>", StringComparison.Ordinal);
|
||||
|
||||
var altRoot = workspaceRoot.Replace("/", "\\", StringComparison.Ordinal);
|
||||
var altRootLower = altRoot.ToLowerInvariant();
|
||||
result = result
|
||||
.Replace(altRoot, "<workspace>", StringComparison.Ordinal)
|
||||
.Replace(altRootLower, "<workspace>", StringComparison.Ordinal);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void NormalizeObservation(JsonObject observation)
|
||||
{
|
||||
if (observation.TryGetPropertyValue("metadata", out var metadataNode) &&
|
||||
metadataNode is JsonObject metadata &&
|
||||
metadata.TryGetPropertyValue("deno.observation.hash", out var hashNode) &&
|
||||
hashNode is JsonValue)
|
||||
{
|
||||
metadata["deno.observation.hash"] = "<hash>";
|
||||
}
|
||||
|
||||
if (observation.TryGetPropertyValue("evidence", out var evidenceNode) &&
|
||||
evidenceNode is JsonArray evidenceArray)
|
||||
{
|
||||
foreach (var evidence in evidenceArray.OfType<JsonObject>())
|
||||
{
|
||||
if (evidence.TryGetPropertyValue("source", out var sourceNode) &&
|
||||
string.Equals(sourceNode?.GetValue<string>(), "deno.observation", StringComparison.OrdinalIgnoreCase) &&
|
||||
evidence.TryGetPropertyValue("sha256", out var shaNode) &&
|
||||
shaNode is JsonValue)
|
||||
{
|
||||
evidence["sha256"] = "<hash>";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SortMetadata(JsonObject component)
|
||||
{
|
||||
if (!component.TryGetPropertyValue("metadata", out var metadataNode) ||
|
||||
metadataNode is not JsonObject metadata)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sorted = new JsonObject();
|
||||
foreach (var entry in metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||
{
|
||||
sorted[entry.Key] = entry.Value?.DeepClone();
|
||||
}
|
||||
|
||||
component["metadata"] = sorted;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptionsProvider = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
BUNDLE_GEMFILE: Gemfile
|
||||
BUNDLE_PATH: vendor/custom-bundle
|
||||
@@ -0,0 +1,20 @@
|
||||
source "https://rubygems.org/"
|
||||
|
||||
gem "rack", "~> 3.1"
|
||||
|
||||
group :web do
|
||||
gem "sinatra", "~> 3.1"
|
||||
gem "pagy"
|
||||
end
|
||||
|
||||
group :jobs do
|
||||
gem "sidekiq", "~> 7.2"
|
||||
end
|
||||
|
||||
group :ops do
|
||||
gem "clockwork"
|
||||
end
|
||||
|
||||
group :tools do
|
||||
gem "pry", "= 0.14.2"
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
clockwork (3.0.0)
|
||||
pagy (6.5.0)
|
||||
pry (0.14.2)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
rack (3.1.2)
|
||||
sidekiq (7.2.1)
|
||||
rack (~> 2.0)
|
||||
sinatra (3.1.0)
|
||||
rack (~> 3.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
clockwork
|
||||
pagy
|
||||
pry (= 0.14.2)
|
||||
rack (~> 3.1)
|
||||
sidekiq (~> 7.2)
|
||||
sinatra (~> 3.1)
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.3
|
||||
@@ -0,0 +1,18 @@
|
||||
require "rack"
|
||||
require "sinatra"
|
||||
require "pagy/backend"
|
||||
require "net/http"
|
||||
require_relative '../config/environment'
|
||||
|
||||
module ConsoleApp
|
||||
class Server < Sinatra::Base
|
||||
get '/' do
|
||||
http = Net::HTTP.new('example.invalid', 443)
|
||||
http.use_ssl = true
|
||||
http.start do |client|
|
||||
client.get('/')
|
||||
end
|
||||
'ok'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
require "pagy"
|
||||
require "json"
|
||||
|
||||
module ConsoleApp
|
||||
module Boot
|
||||
def self.load!
|
||||
JSON.parse('{feature:advisory-ai}')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,202 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "observation::ruby",
|
||||
"name": "Ruby Observation Summary",
|
||||
"type": "ruby-observation",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"ruby.observation.bundler_version": "2.5.3",
|
||||
"ruby.observation.capability.exec": "false",
|
||||
"ruby.observation.capability.net": "true",
|
||||
"ruby.observation.capability.scheduler_list": "clockwork;sidekiq",
|
||||
"ruby.observation.capability.schedulers": "2",
|
||||
"ruby.observation.capability.serialization": "false",
|
||||
"ruby.observation.packages": "6",
|
||||
"ruby.observation.runtime_edges": "5"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "derived",
|
||||
"source": "ruby.observation",
|
||||
"locator": "document",
|
||||
"value": "{\u0022packages\u0022:[{\u0022name\u0022:\u0022clockwork\u0022,\u0022version\u0022:\u00223.0.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022ops\u0022]},{\u0022name\u0022:\u0022pagy\u0022,\u0022version\u0022:\u00226.5.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022web\u0022]},{\u0022name\u0022:\u0022pry\u0022,\u0022version\u0022:\u00220.14.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022tools\u0022]},{\u0022name\u0022:\u0022rack\u0022,\u0022version\u0022:\u00223.1.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022sidekiq\u0022,\u0022version\u0022:\u00227.2.1\u0022,\u0022source\u0022:\u0022vendor\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/custom-bundle/cache/sidekiq-7.2.1.gem\u0022,\u0022groups\u0022:[\u0022jobs\u0022]},{\u0022name\u0022:\u0022sinatra\u0022,\u0022version\u0022:\u00223.1.0\u0022,\u0022source\u0022:\u0022vendor-cache\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/cache/sinatra-3.1.0.gem\u0022,\u0022groups\u0022:[\u0022web\u0022]}],\u0022runtimeEdges\u0022:[{\u0022package\u0022:\u0022clockwork\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022scripts/worker.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022pagy\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app/main.rb\u0022,\u0022config/environment.rb\u0022],\u0022entrypoints\u0022:[\u0022config/environment.rb\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022rack\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022app/main.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022sidekiq\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022scripts/worker.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022sinatra\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022app/main.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]}],\u0022capabilities\u0022:{\u0022usesExec\u0022:false,\u0022usesNetwork\u0022:true,\u0022usesSerialization\u0022:false,\u0022jobSchedulers\u0022:[\u0022clockwork\u0022,\u0022sidekiq\u0022]},\u0022bundledWith\u0022:\u00222.5.3\u0022}",
|
||||
"sha256": "sha256:beaefa12ec1f49e62343781ffa949ec3fa006f0452cf8a342a9a12be3cda1d82"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/clockwork@3.0.0",
|
||||
"purl": "pkg:gem/clockwork@3.0.0",
|
||||
"name": "clockwork",
|
||||
"version": "3.0.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "clockwork;sidekiq",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "ops",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.files": "scripts/worker.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/pagy@6.5.0",
|
||||
"purl": "pkg:gem/pagy@6.5.0",
|
||||
"name": "pagy",
|
||||
"version": "6.5.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "clockwork;sidekiq",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "web",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.entrypoints": "config/environment.rb",
|
||||
"runtime.files": "app/main.rb;config/environment.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/pry@0.14.2",
|
||||
"purl": "pkg:gem/pry@0.14.2",
|
||||
"name": "pry",
|
||||
"version": "0.14.2",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "clockwork;sidekiq",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "tools",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/rack@3.1.2",
|
||||
"purl": "pkg:gem/rack@3.1.2",
|
||||
"name": "rack",
|
||||
"version": "3.1.2",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "clockwork;sidekiq",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.files": "app/main.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/sidekiq@7.2.1",
|
||||
"purl": "pkg:gem/sidekiq@7.2.1",
|
||||
"name": "sidekiq",
|
||||
"version": "7.2.1",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"artifact": "vendor/custom-bundle/cache/sidekiq-7.2.1.gem",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "clockwork;sidekiq",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"declaredOnly": "false",
|
||||
"groups": "jobs",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.files": "scripts/worker.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "vendor"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "sidekiq-7.2.1.gem",
|
||||
"locator": "vendor/custom-bundle/cache/sidekiq-7.2.1.gem"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/sinatra@3.1.0",
|
||||
"purl": "pkg:gem/sinatra@3.1.0",
|
||||
"name": "sinatra",
|
||||
"version": "3.1.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"artifact": "vendor/cache/sinatra-3.1.0.gem",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "clockwork;sidekiq",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"declaredOnly": "false",
|
||||
"groups": "web",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.files": "app/main.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "vendor-cache"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "sinatra-3.1.0.gem",
|
||||
"locator": "vendor/cache/sinatra-3.1.0.gem"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
require "sidekiq"
|
||||
require "clockwork"
|
||||
require "open3"
|
||||
|
||||
module ConsoleApp
|
||||
class Worker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform
|
||||
Clockwork.every(1.hour, 'ping') do
|
||||
Open3.popen3('echo', 'ping') { |_stdin, stdout, _stderr, wait_thr| wait_thr.value }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
ruby "3.1.2"
|
||||
|
||||
gem "rack", "~> 3.0"
|
||||
gem "rails", "7.1.0"
|
||||
gem "puma", "~> 6.1", group: [:web]
|
||||
gem "sqlite3", group: :db
|
||||
|
||||
group :jobs do
|
||||
gem "sidekiq", "7.2.1"
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
gem "pry", "0.14.2"
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem "rspec", "3.12.0"
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
coderay (1.1.3)
|
||||
connection_pool (2.4.1)
|
||||
method_source (1.0.0)
|
||||
pry (0.14.2)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
puma (6.1.1)
|
||||
rack (3.0.8)
|
||||
rails (7.1.0)
|
||||
rspec (3.12.0)
|
||||
sidekiq (7.2.1)
|
||||
connection_pool (>= 2.3.0)
|
||||
rack (~> 2.0)
|
||||
sqlite3 (1.6.0-x86_64-linux)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
pry (= 0.14.2)
|
||||
puma (~> 6.1)
|
||||
rack (~> 3.0)
|
||||
rails (= 7.1.0)
|
||||
rspec (= 3.12.0)
|
||||
sidekiq (= 7.2.1)
|
||||
sqlite3
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.22
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env ruby
|
||||
require "rack"
|
||||
require "puma"
|
||||
require "sidekiq"
|
||||
require "yaml"
|
||||
require "net/http"
|
||||
|
||||
require_relative "app/workers/email_worker"
|
||||
|
||||
class App
|
||||
def call(env)
|
||||
EmailWorker.perform_async(env["PATH_INFO"])
|
||||
[200, { "Content-Type" => "text/plain" }, ["ok"]]
|
||||
end
|
||||
end
|
||||
|
||||
run App.new
|
||||
@@ -0,0 +1,13 @@
|
||||
require "sidekiq"
|
||||
require "net/http"
|
||||
require "yaml"
|
||||
|
||||
class EmailWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(user_id)
|
||||
system("echo sending email #{user_id}")
|
||||
Net::HTTP.get(URI("https://example.com/users/#{user_id}"))
|
||||
YAML.load(File.read("config/clock.rb"))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
require "rack"
|
||||
require_relative "app.rb"
|
||||
|
||||
run App.new
|
||||
@@ -0,0 +1,5 @@
|
||||
require "clockwork"
|
||||
|
||||
module Clockwork
|
||||
every(1.hour, "cleanup") { puts "cleanup" }
|
||||
end
|
||||
@@ -0,0 +1,319 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "observation::ruby",
|
||||
"name": "Ruby Observation Summary",
|
||||
"type": "ruby-observation",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"ruby.observation.bundler_version": "2.4.22",
|
||||
"ruby.observation.capability.exec": "true",
|
||||
"ruby.observation.capability.net": "true",
|
||||
"ruby.observation.capability.scheduler_list": "sidekiq",
|
||||
"ruby.observation.capability.schedulers": "1",
|
||||
"ruby.observation.capability.serialization": "true",
|
||||
"ruby.observation.packages": "11",
|
||||
"ruby.observation.runtime_edges": "3"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "derived",
|
||||
"source": "ruby.observation",
|
||||
"locator": "document",
|
||||
"value": "{\u0022packages\u0022:[{\u0022name\u0022:\u0022coderay\u0022,\u0022version\u0022:\u00221.1.3\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022connection_pool\u0022,\u0022version\u0022:\u00222.4.1\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022method_source\u0022,\u0022version\u0022:\u00221.0.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022pry\u0022,\u0022version\u0022:\u00220.14.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022development\u0022,\u0022test\u0022]},{\u0022name\u0022:\u0022puma\u0022,\u0022version\u0022:\u00226.1.1\u0022,\u0022source\u0022:\u0022vendor-cache\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/cache/puma-6.1.1.gem\u0022,\u0022groups\u0022:[\u0022web\u0022]},{\u0022name\u0022:\u0022rack\u0022,\u0022version\u0022:\u00223.0.8\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022rails\u0022,\u0022version\u0022:\u00227.1.0\u0022,\u0022source\u0022:\u0022vendor-cache\u0022,\u0022platform\u0022:\u0022x86_64-linux\u0022,\u0022declaredOnly\u0022:false,\u0022artifact\u0022:\u0022vendor/cache/rails-7.1.0-x86_64-linux.gem\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022rails\u0022,\u0022version\u0022:\u00227.1.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022rspec\u0022,\u0022version\u0022:\u00223.12.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022test\u0022]},{\u0022name\u0022:\u0022sidekiq\u0022,\u0022version\u0022:\u00227.2.1\u0022,\u0022source\u0022:\u0022vendor-bundle\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/bundle/ruby/3.1.0/gems/sidekiq-7.2.1\u0022,\u0022groups\u0022:[\u0022jobs\u0022]},{\u0022name\u0022:\u0022sqlite3\u0022,\u0022version\u0022:\u00221.6.0-x86_64-linux\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022db\u0022]}],\u0022runtimeEdges\u0022:[{\u0022package\u0022:\u0022puma\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app.rb\u0022],\u0022entrypoints\u0022:[\u0022app.rb\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022rack\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app.rb\u0022,\u0022config.ru\u0022],\u0022entrypoints\u0022:[\u0022app.rb\u0022,\u0022config.ru\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022sidekiq\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app.rb\u0022,\u0022app/workers/email_worker.rb\u0022],\u0022entrypoints\u0022:[\u0022app.rb\u0022,\u0022app/workers/email_worker.rb\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]}],\u0022capabilities\u0022:{\u0022usesExec\u0022:true,\u0022usesNetwork\u0022:true,\u0022usesSerialization\u0022:true,\u0022jobSchedulers\u0022:[\u0022sidekiq\u0022]},\u0022bundledWith\u0022:\u00222.4.22\u0022}",
|
||||
"sha256": "sha256:30b34afcf1a3ae3a32f1088ca535ca5359f9ed1ecf53850909b2bcd4da663ace"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/coderay@1.1.3",
|
||||
"purl": "pkg:gem/coderay@1.1.3",
|
||||
"name": "coderay",
|
||||
"version": "1.1.3",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/connection_pool@2.4.1",
|
||||
"purl": "pkg:gem/connection_pool@2.4.1",
|
||||
"name": "connection_pool",
|
||||
"version": "2.4.1",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/method_source@1.0.0",
|
||||
"purl": "pkg:gem/method_source@1.0.0",
|
||||
"name": "method_source",
|
||||
"version": "1.0.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/pry@0.14.2",
|
||||
"purl": "pkg:gem/pry@0.14.2",
|
||||
"name": "pry",
|
||||
"version": "0.14.2",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "development;test",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/puma@6.1.1",
|
||||
"purl": "pkg:gem/puma@6.1.1",
|
||||
"name": "puma",
|
||||
"version": "6.1.1",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"artifact": "vendor/cache/puma-6.1.1.gem",
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "false",
|
||||
"groups": "web",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.entrypoints": "app.rb",
|
||||
"runtime.files": "app.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "vendor-cache"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "puma-6.1.1.gem",
|
||||
"locator": "vendor/cache/puma-6.1.1.gem"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/rack@3.0.8",
|
||||
"purl": "pkg:gem/rack@3.0.8",
|
||||
"name": "rack",
|
||||
"version": "3.0.8",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.entrypoints": "app.rb;config.ru",
|
||||
"runtime.files": "app.rb;config.ru",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/rails@7.1.0",
|
||||
"purl": "pkg:gem/rails@7.1.0",
|
||||
"name": "rails",
|
||||
"version": "7.1.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"artifact": "vendor/cache/rails-7.1.0-x86_64-linux.gem",
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "false",
|
||||
"groups": "default",
|
||||
"lockfile": "vendor/cache/rails-7.1.0-x86_64-linux.gem",
|
||||
"platform": "x86_64-linux",
|
||||
"source": "vendor-cache"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "rails-7.1.0-x86_64-linux.gem",
|
||||
"locator": "vendor/cache/rails-7.1.0-x86_64-linux.gem"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/rspec@3.12.0",
|
||||
"purl": "pkg:gem/rspec@3.12.0",
|
||||
"name": "rspec",
|
||||
"version": "3.12.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "test",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/sidekiq@7.2.1",
|
||||
"purl": "pkg:gem/sidekiq@7.2.1",
|
||||
"name": "sidekiq",
|
||||
"version": "7.2.1",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"artifact": "vendor/bundle/ruby/3.1.0/gems/sidekiq-7.2.1",
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "false",
|
||||
"groups": "jobs",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.entrypoints": "app.rb;app/workers/email_worker.rb",
|
||||
"runtime.files": "app.rb;app/workers/email_worker.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "vendor-bundle"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "sidekiq-7.2.1",
|
||||
"locator": "vendor/bundle/ruby/3.1.0/gems/sidekiq-7.2.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/sqlite3@1.6.0-x86_64-linux",
|
||||
"purl": "pkg:gem/sqlite3@1.6.0-x86_64-linux",
|
||||
"name": "sqlite3",
|
||||
"version": "1.6.0-x86_64-linux",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "db",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,89 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Ruby;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Ruby.Tests;
|
||||
|
||||
public sealed class RubyLanguageAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SimpleWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzerEmitsObservationPayloadWithSummaryAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
|
||||
var store = new ScanAnalysisStore();
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
var engine = new LanguageAnalyzerEngine(analyzers);
|
||||
var context = new LanguageAnalyzerContext(
|
||||
fixturePath,
|
||||
TimeProvider.System,
|
||||
usageHints: null,
|
||||
services: null,
|
||||
analysisStore: store);
|
||||
|
||||
var result = await engine.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
||||
var snapshots = result.ToSnapshots();
|
||||
|
||||
var summary = Assert.Single(snapshots, snapshot => snapshot.Type == "ruby-observation");
|
||||
Assert.Equal("Ruby Observation Summary", summary.Name);
|
||||
Assert.Equal("observation::ruby", summary.ComponentKey);
|
||||
Assert.True(summary.Metadata.TryGetValue("ruby.observation.packages", out var packageCount));
|
||||
Assert.Equal("11", packageCount);
|
||||
Assert.Equal("3", summary.Metadata["ruby.observation.runtime_edges"]);
|
||||
Assert.Equal("true", summary.Metadata["ruby.observation.capability.exec"]);
|
||||
Assert.Equal("true", summary.Metadata["ruby.observation.capability.net"]);
|
||||
Assert.Equal("true", summary.Metadata["ruby.observation.capability.serialization"]);
|
||||
Assert.Equal("2.4.22", summary.Metadata["ruby.observation.bundler_version"]);
|
||||
|
||||
Assert.True(store.TryGet(ScanAnalysisKeys.RubyObservationPayload, out AnalyzerObservationPayload payload));
|
||||
Assert.Equal("ruby", payload.AnalyzerId);
|
||||
Assert.Equal("ruby.observation", payload.Kind);
|
||||
Assert.Equal("application/json", payload.MediaType);
|
||||
Assert.NotNull(payload.Metadata);
|
||||
Assert.Equal("11", payload.Metadata!["ruby.observation.packages"]);
|
||||
|
||||
using var document = JsonDocument.Parse(payload.Content.ToArray());
|
||||
var root = document.RootElement;
|
||||
var packages = root.GetProperty("packages");
|
||||
Assert.Equal(11, packages.GetArrayLength());
|
||||
|
||||
var runtimeEdges = root.GetProperty("runtimeEdges");
|
||||
Assert.True(runtimeEdges.GetArrayLength() >= 1);
|
||||
|
||||
var capabilities = root.GetProperty("capabilities");
|
||||
Assert.True(capabilities.GetProperty("usesExec").GetBoolean());
|
||||
Assert.True(capabilities.GetProperty("usesNetwork").GetBoolean());
|
||||
Assert.True(capabilities.GetProperty("usesSerialization").GetBoolean());
|
||||
Assert.Equal("2.4.22", root.GetProperty("bundledWith").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ComplexWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "complex-app");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Remove="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Remove="xunit" />
|
||||
<PackageReference Remove="xunit.runner.visualstudio" />
|
||||
<PackageReference Remove="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
<PackageReference Remove="Mongo2Go" />
|
||||
<PackageReference Remove="coverlet.collector" />
|
||||
<PackageReference Remove="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
<ProjectReference Remove="..\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj" />
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\AssemblyInfo.cs" />
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\MongoFixtureCollection.cs" />
|
||||
<Using Remove="StellaOps.Concelier.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit.v3" Version="3.0.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Tests\StellaOps.Scanner.Analyzers.Lang.Tests.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang.Ruby/StellaOps.Scanner.Analyzers.Lang.Ruby.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,6 +1,17 @@
|
||||
// Deterministic Deno workspace exercising vendor, npm, FFI, worker, and fetch flows.
|
||||
{
|
||||
"importMap": "./import_map.json",
|
||||
"imports": {
|
||||
"app/": "./src/",
|
||||
"ffi/": "./src/ffi/",
|
||||
"workers/": "./src/workers/",
|
||||
"npmDynamic": "npm:dayjs@1",
|
||||
"nodeFs": "node:fs",
|
||||
"nodeCrypto": "node:crypto",
|
||||
"nodeWorker": "node:worker_threads",
|
||||
"denoFfi": "deno:ffi",
|
||||
"data": "./data/data.json"
|
||||
},
|
||||
"lock": {
|
||||
"enabled": true,
|
||||
"path": "./deno.lock"
|
||||
|
||||
@@ -1 +1,198 @@
|
||||
"pending"
|
||||
[
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::bundle:<workspace>/bundles/sample.deno",
|
||||
"name": "<workspace>/bundles/sample.deno",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.bundle.entrypoint": "mod.ts",
|
||||
"deno.container.bundle.modules": "2",
|
||||
"deno.container.bundle.resources": "1",
|
||||
"deno.container.identifier": "<workspace>/bundles/sample.deno",
|
||||
"deno.container.kind": "bundle",
|
||||
"deno.container.meta.entrypoint": "mod.ts",
|
||||
"deno.container.meta.moduleCount": "2",
|
||||
"deno.container.meta.resourceCount": "1"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deno.bundle",
|
||||
"locator": "<workspace>/bundles/sample.deno",
|
||||
"value": "mod.ts"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Bundle",
|
||||
"value": "<workspace>/bundles/sample.deno"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::bundle:<workspace>/bundles/sample.eszip",
|
||||
"name": "<workspace>/bundles/sample.eszip",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.bundle.entrypoint": "mod.ts",
|
||||
"deno.container.bundle.modules": "2",
|
||||
"deno.container.bundle.resources": "1",
|
||||
"deno.container.identifier": "<workspace>/bundles/sample.eszip",
|
||||
"deno.container.kind": "bundle",
|
||||
"deno.container.meta.entrypoint": "mod.ts",
|
||||
"deno.container.meta.moduleCount": "2",
|
||||
"deno.container.meta.resourceCount": "1"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deno.bundle",
|
||||
"locator": "<workspace>/bundles/sample.eszip",
|
||||
"value": "mod.ts"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Bundle",
|
||||
"value": "<workspace>/bundles/sample.eszip"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::cache:.deno-<hash>",
|
||||
"name": ".deno-<hash>",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.identifier": ".deno-<hash>",
|
||||
"deno.container.kind": "cache",
|
||||
"deno.container.meta.alias": ".deno-<hash>",
|
||||
"deno.container.meta.kind": "Workspace",
|
||||
"deno.container.meta.path": "<workspace>/.deno"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Cache",
|
||||
"value": ".deno-<hash>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::cache:.deno-<hash>",
|
||||
"name": ".deno-<hash>",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.identifier": ".deno-<hash>",
|
||||
"deno.container.kind": "cache",
|
||||
"deno.container.layerDigest": "deadbeef",
|
||||
"deno.container.meta.alias": ".deno-<hash>",
|
||||
"deno.container.meta.kind": "Layer",
|
||||
"deno.container.meta.path": "<workspace>/layers/sha256-deadbeef/fs/.deno"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Cache",
|
||||
"value": ".deno-<hash>",
|
||||
"sha256": "deadbeef"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::cache:env-deno-<hash>",
|
||||
"name": "env-deno-<hash>",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.identifier": "env-deno-<hash>",
|
||||
"deno.container.kind": "cache",
|
||||
"deno.container.meta.alias": "env-deno-<hash>",
|
||||
"deno.container.meta.kind": "Env",
|
||||
"deno.container.meta.path": "<workspace>/env-deno"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Cache",
|
||||
"value": "env-deno-<hash>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::vendor:vendor-<hash>",
|
||||
"name": "vendor-<hash>",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.identifier": "vendor-<hash>",
|
||||
"deno.container.kind": "vendor",
|
||||
"deno.container.layerDigest": "deadbeef",
|
||||
"deno.container.meta.alias": "vendor-<hash>",
|
||||
"deno.container.meta.path": "<workspace>/layers/sha256-deadbeef/fs/vendor"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Vendor",
|
||||
"value": "vendor-<hash>",
|
||||
"sha256": "deadbeef"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::vendor:vendor-<hash>",
|
||||
"name": "vendor-<hash>",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.identifier": "vendor-<hash>",
|
||||
"deno.container.kind": "vendor",
|
||||
"deno.container.meta.alias": "vendor-<hash>",
|
||||
"deno.container.meta.path": "<workspace>/vendor"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Vendor",
|
||||
"value": "vendor-<hash>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "observation::deno",
|
||||
"name": "Deno Observation Summary",
|
||||
"type": "deno-observation",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.observation.bundles": "2",
|
||||
"deno.observation.capabilities": "1",
|
||||
"deno.observation.entrypoints": "1",
|
||||
"deno.observation.hash": "<hash>"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "derived",
|
||||
"source": "deno.observation",
|
||||
"locator": "document",
|
||||
"value": "{\"entrypoints\":[\"mod.ts\"],\"modules\":[\"./src/\",\"./src/ffi/\",\"./src/workers/\",\"https://api.example.com/data.json\",\"https://cdn.example.com/dynamic/mod.ts\",\"https://deno.land/std@0.207.0/http/server.ts\",\"https://example.com/env.ts\",\"https://example.com/layer.ts\",\"https://import_map.json\",\"https://layer.example/\"],\"capabilities\":[{\"capability\":\"Network\",\"reason\":\"network.remote_module_import\",\"sources\":[\"https://api.example.com/data.json\",\"https://cdn.example.com/dynamic/mod.ts\",\"https://deno.land/std/http/server.ts\",\"https://deno.land/std@0.207.0/http/server.ts\",\"https://example.com/env.ts\",\"https://example.com/layer.ts\",\"https://import_map.json\"]}],\"dynamicImports\":[],\"literalFetches\":[],\"bundles\":[{\"path\":\"<workspace>/bundles/sample.deno\",\"type\":\"deno-compile\",\"entrypoint\":\"mod.ts\",\"modules\":2,\"resources\":1},{\"path\":\"<workspace>/bundles/sample.eszip\",\"type\":\"eszip\",\"entrypoint\":\"mod.ts\",\"modules\":2,\"resources\":1}]}",
|
||||
"sha256": "<hash>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -7,6 +7,7 @@
|
||||
"nodeFs": "node:fs",
|
||||
"nodeCrypto": "node:crypto",
|
||||
"nodeWorker": "node:worker_threads",
|
||||
"denoFfi": "deno:ffi"
|
||||
"denoFfi": "deno:ffi",
|
||||
"data": "./data/data.json"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user