feat: add settings page with accent color toggle

- Add /settings route with SettingsLive view
- Settings stored in localStorage via Settings JS hook
- Accent colors (stat card borders, integrity glow, row hover)
  gated behind body[data-accents] CSS attribute
- Add data-accent attr to stat_card component (replaces inline
  border classes with CSS-driven theming)
- Add Settings nav item to sidebar bottom section
- FOUC prevention: inline script in root layout applies saved
  preference before first paint
This commit is contained in:
Christopher Fahlin
2026-05-13 20:55:51 -07:00
parent de7a3c6c2c
commit ac5c17c66d
11 changed files with 480 additions and 149 deletions
+28
View File
@@ -100,6 +100,34 @@
#sidebar .sidebar-label { display: inline !important; }
}
/* Accent theming — gated behind data-accents on body */
body[data-accents="true"] .stat-card[data-accent="critical"] {
border-left: 2px solid var(--color-red-500);
}
body[data-accents="true"] .stat-card[data-accent="high"] {
border-left: 2px solid var(--color-orange-500);
}
body[data-accents="true"] .stat-card[data-accent="medium"] {
border-left: 2px solid var(--color-yellow-500);
}
body[data-accents="true"] .stat-card[data-accent="low"] {
border-left: 2px solid var(--color-emerald-500);
}
body[data-accents="true"] .stat-card[data-accent="info"] {
border-left: 2px solid var(--color-zinc-500);
}
body[data-accents="true"] .stat-card[data-accent="total"] {
border-left: 2px solid var(--color-sky-500);
}
body[data-accents="true"] .integrity-dot {
box-shadow: 0 0 6px rgba(34, 197, 94, 0.4);
}
body[data-accents="true"] .data-table tbody tr:hover {
background: rgba(255, 255, 255, 0.015);
}
/* Custom scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: #09090b; }
+16 -1
View File
@@ -54,11 +54,26 @@ const StatusBar = {
}
}
const Settings = {
mounted() {
const accents = localStorage.getItem("bulwark-accents") !== "false"
this.pushEvent("settings-loaded", { accents })
this.handleEvent("setting-changed", ({ key, value }) => {
if (key === "accents") {
localStorage.setItem("bulwark-accents", String(value))
document.body.dataset.accents = String(value)
this.pushEvent("settings-loaded", { accents: value })
}
})
}
}
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
hooks: {...colocatedHooks, Sidebar, StatusBar},
hooks: {...colocatedHooks, Sidebar, StatusBar, Settings},
})
topbar.config({barColors: {0: "#71717a"}, shadowColor: "rgba(0, 0, 0, .3)"})
@@ -472,16 +472,24 @@ defmodule SecDashboardWeb.CoreComponents do
## Examples
<.stat_card label="Open Findings" value={42} />
<.stat_card label="Critical" value={5} class="border-l-2 border-l-red-500" />
<.stat_card label="Critical" value={5} accent="critical" />
"""
attr :label, :string, required: true, doc: "the stat description"
attr :value, :any, required: true, doc: "the stat value to display"
attr :accent, :string,
default: nil,
doc: "semantic accent color (critical, high, medium, low, info, total)"
attr :class, :string, default: "", doc: "additional CSS classes"
def stat_card(assigns) do
~H"""
<div class={["rounded-md border border-zinc-800 bg-zinc-900 p-4", @class]}>
<div
class={["stat-card rounded-md border border-zinc-800 bg-zinc-900 p-4", @class]}
data-accent={@accent}
>
<p class="text-xxs font-mono font-medium uppercase tracking-wider text-zinc-500">
{@label}
</p>
+13 -1
View File
@@ -30,7 +30,7 @@ defmodule SecDashboardWeb.Layouts do
attr :active_section, :atom,
default: nil,
doc:
"which sidebar nav item to highlight (:dashboard, :scans, :vulnerabilities, :findings, :assets)"
"which sidebar nav item to highlight (:dashboard, :scans, :vulnerabilities, :findings, :assets, :settings)"
attr :page_title, :string,
default: nil,
@@ -122,6 +122,16 @@ defmodule SecDashboardWeb.Layouts do
</nav>
<div class="border-t border-zinc-800 space-y-0.5 px-2 py-3">
<.link
navigate={~p"/settings"}
class={nav_item_class(:settings, @active_section)}
aria-current={if :settings == @active_section, do: "page"}
title="Settings"
phx-click={JS.dispatch("click", to: "#mobile-nav")}
>
<.icon name="hero-cog-6-tooth" class="size-4 shrink-0" />
<span class="sidebar-label text-xs font-medium">Settings</span>
</.link>
<button
data-sidebar-toggle
class="hidden lg:flex w-full items-center gap-3 rounded-md px-2 py-2 text-zinc-500 hover:bg-zinc-900/50 hover:text-zinc-300"
@@ -260,11 +270,13 @@ defmodule SecDashboardWeb.Layouts do
defp section_label(:vulnerabilities), do: "Vulnerabilities"
defp section_label(:findings), do: "Findings"
defp section_label(:assets), do: "Assets"
defp section_label(:settings), do: "Settings"
defp section_label(_), do: nil
defp section_path("Scans"), do: ~p"/scans"
defp section_path("Vulnerabilities"), do: ~p"/vulnerabilities"
defp section_path("Findings"), do: ~p"/findings"
defp section_path("Assets"), do: ~p"/assets"
defp section_path("Settings"), do: ~p"/settings"
defp section_path(_), do: ~p"/"
end
@@ -18,6 +18,12 @@
</script>
</head>
<body class="h-full bg-zinc-950 text-zinc-50">
<script>
(function() {
var a = localStorage.getItem("bulwark-accents");
document.body.dataset.accents = a === null ? "true" : a;
})();
</script>
{@inner_content}
</body>
</html>
+5 -9
View File
@@ -65,30 +65,26 @@ defmodule SecDashboardWeb.DashboardLive do
</header>
<div class="grid grid-cols-2 gap-3 md:grid-cols-5">
<.stat_card
label="Open Findings"
value={@open_findings}
class="border-l-2 border-l-sky-500"
/>
<.stat_card label="Open Findings" value={@open_findings} accent="total" />
<.stat_card
label="Critical"
value={Map.get(@severity_counts, "critical", 0)}
class="border-l-2 border-l-red-500"
accent="critical"
/>
<.stat_card
label="High"
value={Map.get(@severity_counts, "high", 0)}
class="border-l-2 border-l-orange-500"
accent="high"
/>
<.stat_card
label="Medium"
value={Map.get(@severity_counts, "medium", 0)}
class="border-l-2 border-l-yellow-500"
accent="medium"
/>
<.stat_card
label="Low"
value={Map.get(@severity_counts, "low", 0)}
class="border-l-2 border-l-emerald-500"
accent="low"
/>
</div>
@@ -0,0 +1,82 @@
defmodule SecDashboardWeb.SettingsLive do
@moduledoc """
LiveView for application display settings at `/settings`.
Settings are stored client-side in localStorage via the `Settings` JS hook.
The hook pushes current values to the server on mount so toggles render
correctly, and handles persistence when the user changes a setting.
"""
use SecDashboardWeb, :live_view
@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign(:page_title, "Settings")
|> assign(:active_section, :settings)
|> assign(:accents, true)
{:ok, socket}
end
@impl true
def handle_event("settings-loaded", %{"accents" => accents}, socket) do
{:noreply, assign(socket, :accents, accents)}
end
def handle_event("toggle-accents", _params, socket) do
new_value = !socket.assigns.accents
{:noreply, push_event(socket, "setting-changed", %{key: "accents", value: new_value})}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} active_section={@active_section}>
<div id="settings" phx-hook="Settings" class="mx-auto max-w-3xl space-y-6">
<header>
<h1 class="text-2xl font-bold tracking-tight text-zinc-100">Settings</h1>
<p class="mt-1 text-sm text-zinc-400">
Configure display preferences for the dashboard
</p>
</header>
<section class="rounded-md border border-zinc-800 bg-zinc-900">
<div class="border-b border-zinc-800 px-5 py-3">
<h2 class="text-sm font-semibold text-zinc-100">Display</h2>
</div>
<div class="divide-y divide-zinc-800">
<div class="flex items-center justify-between px-5 py-4">
<div>
<p class="text-sm font-medium text-zinc-200">Accent colors</p>
<p class="mt-0.5 text-xs text-zinc-500">
Show colored borders on stat cards, glow effects, and row highlights
</p>
</div>
<button
phx-click="toggle-accents"
role="switch"
aria-checked={to_string(@accents)}
class={[
"relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border border-zinc-700 transition-colors",
if(@accents, do: "bg-sky-600 border-sky-600", else: "bg-zinc-800")
]}
>
<span class={[
"pointer-events-none inline-block size-3.5 translate-y-px rounded-full bg-white shadow transition-transform",
if(@accents, do: "translate-x-4", else: "translate-x-0.5")
]} />
</button>
</div>
</div>
</section>
<p class="text-xs text-zinc-600">
Settings are stored locally in your browser.
</p>
</div>
</Layouts.app>
"""
end
end
+1
View File
@@ -25,6 +25,7 @@ defmodule SecDashboardWeb.Router do
live "/findings", FindingLive.Index
live "/assets", AssetLive.Index
live "/assets/:id", AssetLive.Show
live "/settings", SettingsLive
end
# Other scopes may use custom stacks.
+64 -31
View File
@@ -1,21 +1,54 @@
{
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "checkov",
"version": "3.1.40",
"semanticVersion": "3.1.40",
"informationUri": "https://www.checkov.io",
"rules": [
{ "id": "CKV_AWS_18", "shortDescription": { "text": "Ensure the S3 bucket has access logging enabled" }, "helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/s3-13-enable-logging" },
{ "id": "CKV_AWS_145", "shortDescription": { "text": "Ensure that S3 buckets are encrypted with KMS by default" }, "helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-general-policies/ensure-that-s3-buckets-are-encrypted-with-kms-by-default" },
{ "id": "CKV_AWS_21", "shortDescription": { "text": "Ensure the S3 bucket has versioning enabled" }, "helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/s3-16-enable-versioning" },
{ "id": "CKV_AWS_23", "shortDescription": { "text": "Ensure every security group and rule has a description" }, "helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-networking-policies/networking-31" },
{ "id": "CKV_AWS_79", "shortDescription": { "text": "Ensure Instance Metadata Service Version 1 is not enabled" }, "helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-general-policies/bc-aws-general-31" },
{ "id": "CKV_AWS_88", "shortDescription": { "text": "EC2 instance should not have public IP" }, "helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-networking-policies/ensure-aws-ec2-instance-does-not-have-a-public-ip" },
{ "id": "CKV_AWS_260", "shortDescription": { "text": "Ensure no security group allows ingress from 0.0.0.0/0 to port 80" }, "helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-networking-policies/ensure-aws-security-group-does-not-allow-ingress-from-0000-to-port-80" },
{ "id": "CKV2_AWS_5", "shortDescription": { "text": "Ensure that Security Groups are attached to an ENI or EC2 instance" }, "helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-networking-policies/ensure-that-security-groups-are-attached-to-ec2-instances-or-elastic-network-interfaces-enis" }
{
"id": "CKV_AWS_18",
"shortDescription": { "text": "Ensure the S3 bucket has access logging enabled" },
"helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/s3-13-enable-logging"
},
{
"id": "CKV_AWS_145",
"shortDescription": { "text": "Ensure that S3 buckets are encrypted with KMS by default" },
"helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-general-policies/ensure-that-s3-buckets-are-encrypted-with-kms-by-default"
},
{
"id": "CKV_AWS_21",
"shortDescription": { "text": "Ensure the S3 bucket has versioning enabled" },
"helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/s3-16-enable-versioning"
},
{
"id": "CKV_AWS_23",
"shortDescription": { "text": "Ensure every security groups rule has a description" },
"helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-networking-policies/networking-31"
},
{
"id": "CKV_AWS_24",
"shortDescription": { "text": "Ensure no security group allows ingress from 0.0.0.0/0 to port 22" },
"helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-networking-policies/networking-1-port-security"
},
{
"id": "CKV_AWS_79",
"shortDescription": { "text": "Ensure Instance Metadata Service Version 1 is not enabled" },
"helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-general-policies/bc-aws-general-31"
},
{
"id": "CKV_AWS_88",
"shortDescription": { "text": "EC2 instance should not have public IP" },
"helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-networking-policies/bc-aws-general-37"
},
{
"id": "CKV_AWS_260",
"shortDescription": { "text": "Ensure no security group allows ingress from 0.0.0.0/0 to port 80" },
"helpUri": "https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-networking-policies/ensure-aws-security-group-does-not-allow-ingress-from-0-0-0-0-0-to-port-80"
}
]
}
},
@@ -23,50 +56,50 @@
{
"ruleId": "CKV_AWS_18",
"level": "warning",
"message": { "text": "S3 bucket 'data-lake-raw' does not have access logging enabled" },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/storage/main.tf" }, "region": { "startLine": 12 } } }]
"message": { "text": "S3 bucket 'data-lake-raw' does not have access logging enabled. Enable access logging to track requests for compliance and security auditing." },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/storage/s3.tf" }, "region": { "startLine": 12, "startColumn": 1, "endLine": 24, "endColumn": 1 } } }]
},
{
"ruleId": "CKV_AWS_145",
"level": "warning",
"message": { "text": "S3 bucket 'data-lake-raw' is not encrypted with KMS" },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/storage/main.tf" }, "region": { "startLine": 12 } } }]
"message": { "text": "S3 bucket 'data-lake-raw' is not encrypted with a KMS customer managed key. Use aws_kms_key for server-side encryption." },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/storage/s3.tf" }, "region": { "startLine": 12, "startColumn": 1, "endLine": 24, "endColumn": 1 } } }]
},
{
"ruleId": "CKV_AWS_21",
"level": "warning",
"message": { "text": "S3 bucket 'data-lake-raw' does not have versioning enabled" },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/storage/main.tf" }, "region": { "startLine": 12 } } }]
"message": { "text": "S3 bucket 'artifacts-prod' does not have versioning enabled. Enable versioning to preserve, retrieve, and restore every version of every object." },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/storage/s3.tf" }, "region": { "startLine": 42, "startColumn": 1, "endLine": 56, "endColumn": 1 } } }]
},
{
"ruleId": "CKV_AWS_23",
"level": "note",
"message": { "text": "Security group 'api-sg' does not have a description" },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/networking/security_groups.tf" }, "region": { "startLine": 1 } } }]
"message": { "text": "Security group 'web-sg' rule does not have a description. Add descriptions to all rules for auditability." },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/network/security_groups.tf" }, "region": { "startLine": 8, "startColumn": 1, "endLine": 22, "endColumn": 1 } } }]
},
{
"ruleId": "CKV_AWS_24",
"level": "error",
"message": { "text": "Security group 'web-sg' allows SSH access from 0.0.0.0/0. Restrict SSH access to known IP ranges." },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/network/security_groups.tf" }, "region": { "startLine": 15, "startColumn": 3, "endLine": 20, "endColumn": 3 } } }]
},
{
"ruleId": "CKV_AWS_79",
"level": "error",
"message": { "text": "EC2 instance 'worker-node' has IMDSv1 enabled" },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/compute/main.tf" }, "region": { "startLine": 45 } } }]
"message": { "text": "EC2 instance 'api-server' has IMDSv1 enabled. Enforce IMDSv2 to prevent SSRF-based credential theft." },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/compute/ec2.tf" }, "region": { "startLine": 1, "startColumn": 1, "endLine": 32, "endColumn": 1 } } }]
},
{
"ruleId": "CKV_AWS_88",
"level": "error",
"message": { "text": "EC2 instance 'bastion' has a public IP address assigned" },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/compute/bastion.tf" }, "region": { "startLine": 8 } } }]
"message": { "text": "EC2 instance 'api-server' has associate_public_ip_address set to true. Use a NAT gateway or load balancer instead of a public IP." },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/compute/ec2.tf" }, "region": { "startLine": 14, "startColumn": 3, "endLine": 14, "endColumn": 42 } } }]
},
{
"ruleId": "CKV_AWS_260",
"level": "error",
"message": { "text": "Security group 'api-sg' allows ingress from 0.0.0.0/0 to port 80" },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/networking/security_groups.tf" }, "region": { "startLine": 15 } } }]
},
{
"ruleId": "CKV2_AWS_5",
"level": "note",
"message": { "text": "Security group 'legacy-db-sg' is not attached to any ENI or EC2 instance" },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/networking/security_groups.tf" }, "region": { "startLine": 42 } } }]
"level": "warning",
"message": { "text": "Security group 'alb-sg' allows ingress from 0.0.0.0/0 to port 80. Consider restricting to HTTPS only (port 443) with a redirect." },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "modules/network/security_groups.tf" }, "region": { "startLine": 34, "startColumn": 3, "endLine": 40, "endColumn": 3 } } }]
}
]
}
+43 -17
View File
@@ -1,18 +1,39 @@
{
"$schema": "https://json.schemastore.org/sarif-2.1.0-rtm.5.json",
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "gitleaks",
"version": "8.18.1",
"semanticVersion": "8.18.1",
"informationUri": "https://github.com/gitleaks/gitleaks",
"rules": [
{ "id": "generic-api-key", "shortDescription": { "text": "Generic API Key" }, "helpUri": "https://github.com/gitleaks/gitleaks/blob/master/cmd/generate/config/rules/generic.go" },
{ "id": "aws-access-key-id", "shortDescription": { "text": "AWS Access Key ID" }, "helpUri": "https://github.com/gitleaks/gitleaks/blob/master/cmd/generate/config/rules/aws.go" },
{ "id": "private-key", "shortDescription": { "text": "Private Key" }, "helpUri": "https://github.com/gitleaks/gitleaks/blob/master/cmd/generate/config/rules/privatekey.go" },
{ "id": "github-pat", "shortDescription": { "text": "GitHub Personal Access Token" }, "helpUri": "https://github.com/gitleaks/gitleaks/blob/master/cmd/generate/config/rules/github.go" },
{ "id": "slack-webhook-url", "shortDescription": { "text": "Slack Webhook URL" }, "helpUri": "https://github.com/gitleaks/gitleaks/blob/master/cmd/generate/config/rules/slack.go" }
{
"id": "generic-api-key",
"shortDescription": { "text": "Generic API Key" },
"helpUri": "https://github.com/gitleaks/gitleaks/blob/master/cmd/generate/config/rules/generic.go"
},
{
"id": "aws-access-key-id",
"shortDescription": { "text": "AWS Access Key ID" },
"helpUri": "https://github.com/gitleaks/gitleaks/blob/master/cmd/generate/config/rules/aws.go"
},
{
"id": "private-key",
"shortDescription": { "text": "Private Key" },
"helpUri": "https://github.com/gitleaks/gitleaks/blob/master/cmd/generate/config/rules/privatekey.go"
},
{
"id": "github-pat",
"shortDescription": { "text": "GitHub Personal Access Token" },
"helpUri": "https://github.com/gitleaks/gitleaks/blob/master/cmd/generate/config/rules/github.go"
},
{
"id": "slack-webhook-url",
"shortDescription": { "text": "Slack Webhook URL" },
"helpUri": "https://github.com/gitleaks/gitleaks/blob/master/cmd/generate/config/rules/slack.go"
}
]
}
},
@@ -20,32 +41,37 @@
{
"ruleId": "generic-api-key",
"level": "error",
"message": { "text": "Generic API Key detected in environment file" },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": ".env" }, "region": { "startLine": 3, "startColumn": 16, "endColumn": 48 } } }]
"message": { "text": "Generic API Key detected: matched pattern for SendGrid API key in environment configuration file." },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": ".env" }, "region": { "startLine": 3, "startColumn": 16, "endLine": 3, "endColumn": 85, "snippet": { "text": "SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } }],
"partialFingerprints": { "primaryLocationLineHash": "d4e5f6a7b8c9d0e1" }
},
{
"ruleId": "aws-access-key-id",
"level": "error",
"message": { "text": "AWS Access Key ID found in configuration" },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "config/deploy.py" }, "region": { "startLine": 14, "startColumn": 22, "endColumn": 42 } } }]
"message": { "text": "AWS Access Key ID detected in Python deployment script. Rotate this key immediately and use IAM roles or environment variables." },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "scripts/deploy.py" }, "region": { "startLine": 14, "startColumn": 22, "endLine": 14, "endColumn": 42, "snippet": { "text": "aws_access_key_id = \"AKIAIOSFODNN7EXAMPLE\"" } } } }],
"partialFingerprints": { "primaryLocationLineHash": "a1b2c3d4e5f6a7b8" }
},
{
"ruleId": "private-key",
"level": "error",
"message": { "text": "Private key detected in source tree" },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "certs/server.key" }, "region": { "startLine": 1, "startColumn": 1, "endColumn": 32 } } }]
"message": { "text": "RSA private key detected embedded directly in JavaScript source file. Move to a secrets manager or encrypted storage." },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "src/auth/keys.js" }, "region": { "startLine": 8, "startColumn": 1, "endLine": 28, "endColumn": 1, "snippet": { "text": "const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----" } } } }],
"partialFingerprints": { "primaryLocationLineHash": "c3d4e5f6a7b8c9d0" }
},
{
"ruleId": "github-pat",
"level": "error",
"message": { "text": "GitHub Personal Access Token found in CI config" },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": ".github/workflows/deploy.yml" }, "region": { "startLine": 28, "startColumn": 18, "endColumn": 58 } } }]
"message": { "text": "GitHub Personal Access Token found hardcoded in CI/CD workflow configuration. Use GitHub Actions secrets instead." },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": ".github/workflows/deploy.yml" }, "region": { "startLine": 22, "startColumn": 18, "endLine": 22, "endColumn": 58, "snippet": { "text": "token: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } }],
"partialFingerprints": { "primaryLocationLineHash": "e5f6a7b8c9d0e1f2" }
},
{
"ruleId": "slack-webhook-url",
"level": "warning",
"message": { "text": "Slack Webhook URL detected in notification handler" },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "scripts/notify.js" }, "region": { "startLine": 7, "startColumn": 24, "endColumn": 96 } } }]
"message": { "text": "Slack Webhook URL detected in notification service configuration. Move to environment variable to prevent unauthorized message posting." },
"locations": [{ "physicalLocation": { "artifactLocation": { "uri": "src/notifications/slack.py" }, "region": { "startLine": 5, "startColumn": 15, "endLine": 5, "endColumn": 89, "snippet": { "text": "WEBHOOK_URL = \"https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX\"" } } } }],
"partialFingerprints": { "primaryLocationLineHash": "f6a7b8c9d0e1f2a3" }
}
]
}
+212 -88
View File
@@ -3,125 +3,249 @@
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "api-server-sbom",
"documentNamespace": "https://spdx.org/spdxdocs/api-server-sbom-v2.4.1",
"documentNamespace": "https://spdx.org/spdxdocs/api-server-sbom-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"creationInfo": {
"created": "2026-05-10T08:30:00Z",
"creators": ["Tool: syft-0.105.0", "Organization: myorg"]
"created": "2024-11-15T14:22:00Z",
"creators": [
"Tool: syft-0.105.0",
"Organization: myorg"
],
"licenseListVersion": "3.22"
},
"packages": [
{
"SPDXID": "SPDXRef-Package-flask",
"name": "api-server",
"SPDXID": "SPDXRef-Package-api-server",
"versionInfo": "2.4.1",
"downloadLocation": "https://github.com/myorg/api-server",
"primaryPackagePurpose": "APPLICATION",
"supplier": "Organization: myorg",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/api-server@2.4.1"
}
]
},
{
"name": "flask",
"SPDXID": "SPDXRef-Package-flask",
"versionInfo": "3.0.0",
"downloadLocation": "https://pypi.org/project/Flask/3.0.0/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/flask@3.0.0" }]
"supplier": "Organization: Pallets",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/flask@3.0.0"
}
]
},
{
"SPDXID": "SPDXRef-Package-werkzeug",
"name": "werkzeug",
"SPDXID": "SPDXRef-Package-werkzeug",
"versionInfo": "3.0.1",
"downloadLocation": "https://pypi.org/project/Werkzeug/3.0.1/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/werkzeug@3.0.1" }]
"supplier": "Organization: Pallets",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/werkzeug@3.0.1"
}
]
},
{
"SPDXID": "SPDXRef-Package-jinja2",
"name": "jinja2",
"SPDXID": "SPDXRef-Package-jinja2",
"versionInfo": "3.1.2",
"downloadLocation": "https://pypi.org/project/Jinja2/3.1.2/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/jinja2@3.1.2" }]
"supplier": "Organization: Pallets",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/jinja2@3.1.2"
}
]
},
{
"SPDXID": "SPDXRef-Package-requests",
"name": "requests",
"versionInfo": "2.31.0",
"downloadLocation": "https://pypi.org/project/requests/2.31.0/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/requests@2.31.0" }]
},
{
"SPDXID": "SPDXRef-Package-urllib3",
"name": "urllib3",
"versionInfo": "2.1.0",
"downloadLocation": "https://pypi.org/project/urllib3/2.1.0/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/urllib3@2.1.0" }]
},
{
"SPDXID": "SPDXRef-Package-sqlalchemy",
"name": "sqlalchemy",
"versionInfo": "2.0.23",
"downloadLocation": "https://pypi.org/project/SQLAlchemy/2.0.23/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/sqlalchemy@2.0.23" }]
},
{
"SPDXID": "SPDXRef-Package-pydantic",
"name": "pydantic",
"versionInfo": "2.5.2",
"downloadLocation": "https://pypi.org/project/pydantic/2.5.2/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/pydantic@2.5.2" }]
},
{
"SPDXID": "SPDXRef-Package-celery",
"name": "celery",
"versionInfo": "5.3.6",
"downloadLocation": "https://pypi.org/project/celery/5.3.6/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/celery@5.3.6" }]
},
{
"SPDXID": "SPDXRef-Package-redis",
"name": "redis",
"versionInfo": "5.0.1",
"downloadLocation": "https://pypi.org/project/redis/5.0.1/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/redis@5.0.1" }]
},
{
"SPDXID": "SPDXRef-Package-cryptography",
"name": "cryptography",
"versionInfo": "41.0.7",
"downloadLocation": "https://pypi.org/project/cryptography/41.0.7/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/cryptography@41.0.7" }]
},
{
"SPDXID": "SPDXRef-Package-gunicorn",
"name": "gunicorn",
"versionInfo": "21.2.0",
"downloadLocation": "https://pypi.org/project/gunicorn/21.2.0/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/gunicorn@21.2.0" }]
},
{
"SPDXID": "SPDXRef-Package-psycopg2",
"name": "psycopg2-binary",
"versionInfo": "2.9.9",
"downloadLocation": "https://pypi.org/project/psycopg2-binary/2.9.9/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/psycopg2-binary@2.9.9" }]
},
{
"SPDXID": "SPDXRef-Package-markupsafe",
"name": "markupsafe",
"SPDXID": "SPDXRef-Package-markupsafe",
"versionInfo": "2.1.3",
"downloadLocation": "https://pypi.org/project/MarkupSafe/2.1.3/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/markupsafe@2.1.3" }]
"supplier": "Organization: Pallets",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/markupsafe@2.1.3"
}
]
},
{
"SPDXID": "SPDXRef-Package-click",
"name": "click",
"versionInfo": "8.1.7",
"downloadLocation": "https://pypi.org/project/click/8.1.7/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/click@8.1.7" }]
"name": "requests",
"SPDXID": "SPDXRef-Package-requests",
"versionInfo": "2.31.0",
"downloadLocation": "https://pypi.org/project/requests/2.31.0/",
"supplier": "Organization: Python Software Foundation",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/requests@2.31.0"
}
]
},
{
"name": "urllib3",
"SPDXID": "SPDXRef-Package-urllib3",
"versionInfo": "2.1.0",
"downloadLocation": "https://pypi.org/project/urllib3/2.1.0/",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/urllib3@2.1.0"
}
]
},
{
"SPDXID": "SPDXRef-Package-certifi",
"name": "certifi",
"SPDXID": "SPDXRef-Package-certifi",
"versionInfo": "2023.11.17",
"downloadLocation": "https://pypi.org/project/certifi/2023.11.17/",
"externalRefs": [{ "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:pypi/certifi@2023.11.17" }]
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/certifi@2023.11.17"
}
]
},
{
"name": "idna",
"SPDXID": "SPDXRef-Package-idna",
"versionInfo": "3.6",
"downloadLocation": "https://pypi.org/project/idna/3.6/",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/idna@3.6"
}
]
},
{
"name": "sqlalchemy",
"SPDXID": "SPDXRef-Package-sqlalchemy",
"versionInfo": "2.0.23",
"downloadLocation": "https://pypi.org/project/SQLAlchemy/2.0.23/",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/sqlalchemy@2.0.23"
}
]
},
{
"name": "pyjwt",
"SPDXID": "SPDXRef-Package-pyjwt",
"versionInfo": "2.8.0",
"downloadLocation": "https://pypi.org/project/PyJWT/2.8.0/",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/pyjwt@2.8.0"
}
]
},
{
"name": "gunicorn",
"SPDXID": "SPDXRef-Package-gunicorn",
"versionInfo": "21.2.0",
"downloadLocation": "https://pypi.org/project/gunicorn/21.2.0/",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/gunicorn@21.2.0"
}
]
},
{
"name": "celery",
"SPDXID": "SPDXRef-Package-celery",
"versionInfo": "5.3.6",
"downloadLocation": "https://pypi.org/project/celery/5.3.6/",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/celery@5.3.6"
}
]
},
{
"name": "redis",
"SPDXID": "SPDXRef-Package-redis",
"versionInfo": "5.0.1",
"downloadLocation": "https://pypi.org/project/redis/5.0.1/",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/redis@5.0.1"
}
]
},
{
"name": "cryptography",
"SPDXID": "SPDXRef-Package-cryptography",
"versionInfo": "41.0.7",
"downloadLocation": "https://pypi.org/project/cryptography/41.0.7/",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/cryptography@41.0.7"
}
]
}
],
"relationships": [
{ "spdxElementId": "SPDXRef-DOCUMENT", "relatedSpdxElement": "SPDXRef-Package-flask", "relationshipType": "DESCRIBES" },
{ "spdxElementId": "SPDXRef-Package-flask", "relatedSpdxElement": "SPDXRef-Package-werkzeug", "relationshipType": "DEPENDS_ON" },
{ "spdxElementId": "SPDXRef-Package-flask", "relatedSpdxElement": "SPDXRef-Package-jinja2", "relationshipType": "DEPENDS_ON" },
{ "spdxElementId": "SPDXRef-Package-flask", "relatedSpdxElement": "SPDXRef-Package-click", "relationshipType": "DEPENDS_ON" },
{ "spdxElementId": "SPDXRef-Package-flask", "relatedSpdxElement": "SPDXRef-Package-markupsafe", "relationshipType": "DEPENDS_ON" },
{ "spdxElementId": "SPDXRef-Package-requests", "relatedSpdxElement": "SPDXRef-Package-urllib3", "relationshipType": "DEPENDS_ON" },
{ "spdxElementId": "SPDXRef-Package-requests", "relatedSpdxElement": "SPDXRef-Package-certifi", "relationshipType": "DEPENDS_ON" }
{ "spdxElementId": "SPDXRef-DOCUMENT", "relationshipType": "DESCRIBES", "relatedSpdxElement": "SPDXRef-Package-api-server" },
{ "spdxElementId": "SPDXRef-Package-api-server", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-flask" },
{ "spdxElementId": "SPDXRef-Package-api-server", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-requests" },
{ "spdxElementId": "SPDXRef-Package-api-server", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-sqlalchemy" },
{ "spdxElementId": "SPDXRef-Package-api-server", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-pyjwt" },
{ "spdxElementId": "SPDXRef-Package-api-server", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-gunicorn" },
{ "spdxElementId": "SPDXRef-Package-api-server", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-celery" },
{ "spdxElementId": "SPDXRef-Package-api-server", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-redis" },
{ "spdxElementId": "SPDXRef-Package-api-server", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-cryptography" },
{ "spdxElementId": "SPDXRef-Package-flask", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-werkzeug" },
{ "spdxElementId": "SPDXRef-Package-flask", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-jinja2" },
{ "spdxElementId": "SPDXRef-Package-jinja2", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-markupsafe" },
{ "spdxElementId": "SPDXRef-Package-requests", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-urllib3" },
{ "spdxElementId": "SPDXRef-Package-requests", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-certifi" },
{ "spdxElementId": "SPDXRef-Package-requests", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-Package-idna" }
]
}