Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions bazel/ui.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def _pl_webpack_deps_impl(ctx):

cmd = ui_shared_cmds_start + cp_cmds + [
'pushd "$TMPPATH/src/ui" &> /dev/null',
"yarn install --immutable &> build.log",
"/opt/px_dev/tools/node/bin/yarn install --immutable &> build.log",
# Pick a deterministic mtime so that the output is not volatile.
# This helps ensure that bazel can cache the ui builds as expected.
'tar --mtime="2018-01-01 00:00:00 UTC" -czf "$BASE_PATH/{}" .'.format(out.path),
Expand Down Expand Up @@ -87,9 +87,7 @@ def _pl_webpack_library_impl(ctx):
'pushd "$TMPPATH/src/ui" &> /dev/null',
'tar -xzf "$BASE_PATH/{}"'.format(ctx.file.deps.path),
'mv -f "$BASE_PATH/{}" src/pages/credits/licenses.json'.format(ctx.file.licenses.path),
"retval=0",
"output=`yarn build_prod 2>&1` || retval=$?",
'[ "$retval" -eq 0 ] || (echo $output; echo "Build Failed with Code: $retval"; exit $retval)',
"/opt/px_dev/tools/node/bin/yarn build_prod",
'cp dist/bundle.tar.gz "$BASE_PATH/{}"'.format(out.path),
] + ui_shared_cmds_finish

Expand Down Expand Up @@ -165,8 +163,8 @@ def _pl_deps_licenses_impl(ctx):
'pushd "$TMPPATH/src/ui" &> /dev/null',
'export LIC_TMPPATH="$(mktemp -d)"',
'tar -xzf "$BASE_PATH/{}"'.format(ctx.file.deps.path),
"yarn license_check --excludePrivatePackages --production --json --out $LIC_TMPPATH/checker.json",
'yarn pnpify node ./tools/licenses/yarn_license_extractor.js --input=$LIC_TMPPATH/checker.json --output="$BASE_PATH/{}"'.format(out.path),
"/opt/px_dev/tools/node/bin/yarn license_check --excludePrivatePackages --production --json --out $LIC_TMPPATH/checker.json",
'/opt/px_dev/tools/node/bin/yarn pnpify node ./tools/licenses/yarn_license_extractor.js --input=$LIC_TMPPATH/checker.json --output="$BASE_PATH/{}"'.format(out.path),
] + ui_shared_cmds_finish

ctx.actions.run_shell(
Expand Down
2 changes: 1 addition & 1 deletion private/cockpit/script_bundles_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ metadata:
data:
SCRIPT_BUNDLE_URLS: >-
[
"https://k8sstormcenter.github.io/pixie/pxl_scripts/bundle.json"
"/bundle-oss.json"
]
SCRIPT_BUNDLE_DEV: "false"
PL_SCRIPT_MODIFICATION_DISABLED: "false"
21 changes: 21 additions & 0 deletions src/api/proto/vispb/vis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,12 @@ message Graph {
// The value at which the edge should be considered 'HIGH'.
int64 high_threshold = 2;
}
message NodeThresholds {
// The value at which the node should be considered 'MEDIUM'.
int64 medium_threshold = 1;
// The value at which the node should be considered 'HIGH'.
int64 high_threshold = 2;
}
oneof input {
// The column which contains the dot-formatted graph file to render.
string dot_column = 1;
Expand All @@ -399,6 +405,21 @@ message Graph {
int64 edge_length = 8;
// Whether the graph should start in hierarchy mode as a default.
bool enable_default_hierarchy = 9;
// The column whose value is rendered as a persistent label on the edge. Optional.
// When unset, edges have no label (current behaviour).
string edge_label_column = 10;
// The column whose value overrides the default node id label. Optional.
// When unset, the node id is used as the label (current behaviour).
string node_label_column = 11;
// The column to use to determine what the color (fill) of the node should be. Optional.
// Interpreted via NodeThresholds when set; falls back to the SemanticType-derived shape
// color when unset.
string node_color_column = 12;
// The threshold at which node values are classified as a 'LOW'/'MEDIUM'/'HIGH' color.
// Optional, but must have a NodeColorColumn specified.
NodeThresholds node_thresholds = 13;
// The columns to display when hovering over a node. Optional.
repeated string node_hover_info = 14;
}

// Display traffic between pods or services as a graph.
Expand Down
7 changes: 4 additions & 3 deletions src/pxl_scripts/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ genrule(
],
outs = ["bundle-oss.json"],
cmd = """
export PATH_PREFIX=$$(dirname $(location //src/pxl_scripts:Makefile))/;
EXECUTABLES=../../$(location //src/pixie_cli:px) make -C $$PATH_PREFIX bundle-oss.json;
cp bundle-oss.json $(@D)/bundle-oss.json
MAKEDIR=$$(dirname $(location //src/pxl_scripts:Makefile));
PX_ABS=$$(pwd)/$(location //src/pixie_cli:px);
( cd $$MAKEDIR && EXECUTABLES=$$PX_ABS make PATH_PREFIX= bundle-oss.json );
cp $$MAKEDIR/bundle-oss.json $(@D)/bundle-oss.json
""",
tools = [
"//src/pixie_cli:px",
Expand Down
89 changes: 89 additions & 0 deletions src/pxl_scripts/px/dx_evidence_graph/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# dx_evidence_graph

A Pixie UI dashboard that renders one dx-agent investigation as a
**severity-weighted, all-protocol pod-to-pod attack graph**. Replaces
the latency-weighted HTTP service map in `cluster_overview` for
security work.

* Nodes = pods. Falls back to service → IP, mirroring `net_flow_graph`.
* Edges = the attack path emitted by dx (delivery → egress →
execution → collection → exfil → pivot).
* Display spec: `vispb.Graph`. **`edgeWeightColumn = weight`**
(open-ended UInt16 sum of CRS severity → edge thickness),
**`edgeColorColumn = max_severity`** (discrete 2-5 heat → edge
colour).
* Read source: `forensic_db.dx_attack_graph` via `px.DataFrame`'s
`clickhouse_dsn` kwarg (`src/carnot/planner/objects/dataframe.cc:43`).

## Schema — `forensic_db.dx_attack_graph`

Locked with dx-agent in PR #62 / `entlein/dx#68`. The
`attackgraph.Edge` Go struct is the single source of truth for the
JSON wire format, the ClickHouse row, and the test fixture.

| Column | Type | Role |
|---|---|---|
| `investigation_id` | String | one graph per dx verdict / pivot incident (UI filter key) |
| `ts` | UInt64 | unix nanos |
| `requestor_pod` / `responder_pod` | String | the hop (`ns/pod`); `""` if only an IP is known |
| `requestor_service` / `responder_service` | String | |
| `requestor_ip` / `responder_ip` | String | peer IP when pod unresolved |
| `weight` | UInt16 | Σ CRS severity on the hop — `edgeWeightColumn` |
| `max_severity` | UInt8 | top single-criterion severity (2-5) — `edgeColorColumn` |
| `confidence` | Float32 | verdict confidence |
| `edge_kind` | String | `delivery`/`egress`/`execution`/`collection`/`exfil`/`pivot` |
| `condition` / `criteria` | String | ruled-in condition + criterion label(s) |
| `num_findings` | UInt32 | |

Table DDL (mirrors `kubescape_logs` partition/TTL convention):

```sql
CREATE TABLE forensic_db.dx_attack_graph ( ...columns above... )
ENGINE = MergeTree
PARTITION BY toYYYYMM(fromUnixTimestamp64Nano(ts))
ORDER BY (investigation_id, requestor_pod, responder_pod)
TTL toDateTime(fromUnixTimestamp64Nano(ts)) + INTERVAL 30 DAY DELETE;
```

## Per-rig ClickHouse DSN

The bundled `vis.json` ships with `clickhouse_dsn` **empty** — the
default is intentionally non-credentialed so the bundle stays
portable across clusters. Operators fill the DSN in via the Pixie
UI script-args panel at run time.

For the in-cluster soc deployment the DSN is:

```
forensic_analyst:changeme-analyst@clickhouse-forensic-soc-db.clickhouse.svc.cluster.local:9000/forensic_db
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +57 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to the fenced DSN block.

Line 57 opens a fenced code block without a language, which violates MD040 and may fail markdown lint gates.

Suggested fix
-```
+```text
 forensic_analyst:changeme-analyst@clickhouse-forensic-soc-db.clickhouse.svc.cluster.local:9000/forensic_db
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>

[warning] 57-57: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @src/pxl_scripts/px/dx_evidence_graph/README.md around lines 57 - 59, The
fenced code block containing the ClickHouse forensic database DSN example
(starting with forensic_analyst:changeme-analyst@...) is missing a language tag
on the opening fence, which violates the MD040 markdown lint rule. Add the
"text" language identifier to the opening triple backticks (change ``` to

Source: Linters/SAST tools


`forensic_analyst` has read-only SELECT on `forensic_db`; same
credential the existing `soc/analysis/px_clickhouse/kubescape/observe.pxl`
script uses for `kubescape_logs`. Override in the UI for other rigs.

## Deploy

Bundle build path:

1. `//src/pxl_scripts:script_bundle` walks every `*.pxl` + `vis.json`
under `src/pxl_scripts/` and emits `bundle-oss.json`
(`src/pxl_scripts/BUILD.bazel:34`).
2. `//src/cloud/proxy:proxy_server_image` bakes the bundle in as a
container layer at `/bundle`
(`src/cloud/proxy/BUILD.bazel:36`).
3. `skaffold run -f skaffold/skaffold_cloud.yaml` rebuilds the
cloud-proxy image and applies the Deployment.

Vizier / PEM / standalone-pem images are unaffected — this is a
UI-bundle-only change.

## Out of scope for v1

* `conn_stats` overlay (the "render the benign neighbourhood + light
up the attack path" view). Ship the attack-path-only graph first;
add the join in v2 once the visual has been used on a real
incident.
* Time anchoring relative to `ts` rather than free-form `start_time`.
Operators today use `-15m` defaults; a future widget could centre
the window on the investigation's first `ts`.
39 changes: 39 additions & 0 deletions src/pxl_scripts/px/dx_evidence_graph/dx_evidence_graph.pxl
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright 2018- The Pixie Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0

import px


def dx_attack_graph(start_time: str, clickhouse_dsn: str, table: str):
df = px.DataFrame(table, clickhouse_dsn=clickhouse_dsn, start_time=start_time)
# Node identity: pod, else service, else IP. Edges whose peer is an IP or a
# non-pod entity (the k8s API server, an external endpoint, a consulted
# socket) have an empty *_pod; keying the graph on *_pod alone collapses ALL
# of them into one bogus "" node. Coalesce so each distinct peer is its own
# node — same idiom Pixie's net_flow_graph uses (px.select(src=='', src_ip, src)).
df.requestor = px.select(df.requestor_pod == '',
px.select(df.requestor_service == '', df.requestor_ip, df.requestor_service),
df.requestor_pod)
df.responder = px.select(df.responder_pod == '',
px.select(df.responder_service == '', df.responder_ip, df.responder_service),
df.responder_pod)
return df[['requestor', 'responder',
'requestor_pod', 'responder_pod',
'requestor_service', 'responder_service',
'requestor_ip', 'responder_ip',
'weight', 'max_severity', 'confidence',
'edge_kind', 'condition', 'criteria', 'num_findings',
'investigation_id']]
9 changes: 9 additions & 0 deletions src/pxl_scripts/px/dx_evidence_graph/manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
short: DX Attack Graph
long: >
Severity-weighted, all-protocol pod-to-pod attack graph for one
dx-agent investigation. Renders attackgraph.Edge records emitted by
dx with weight (sum of CRS evidence severity) on the edges and
max_severity colouring the heat. v0 manual-load only — wires up to
the dx_attack_graph ClickHouse / Pixie ingest in v1. See README.md
in this directory.
75 changes: 75 additions & 0 deletions src/pxl_scripts/px/dx_evidence_graph/vis.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"variables": [
{
"name": "start_time",
"type": "PX_STRING",
"description": "Start time of the window.",
"defaultValue": "-15m"
},
{
"name": "clickhouse_dsn",
"type": "PX_STRING",
"description": "ClickHouse DSN: user:pass@host:port/db.",
"defaultValue": "forensic_analyst:changeme-analyst@clickhouse-forensic-soc-db.clickhouse.svc.cluster.local:9000/forensic_db"
},
{
"name": "table",
"type": "PX_STRING",
"description": "dx_attack_graph_malicious (default; rule-ins only — benign is NOT pulled from ClickHouse) or dx_attack_graph (full table, includes benign).",
"defaultValue": "dx_attack_graph_malicious"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
],
"globalFuncs": [
{
"outputName": "dx_graph",
"func": {
"name": "dx_attack_graph",
"args": [
{"name": "start_time", "variable": "start_time"},
{"name": "clickhouse_dsn", "variable": "clickhouse_dsn"},
{"name": "table", "variable": "table"}
]
}
}
],
"widgets": [
{
"name": "DX Attack Graph",
"position": {"x": 0, "y": 0, "w": 12, "h": 5},
"globalFuncOutputName": "dx_graph",
"displaySpec": {
"@type": "types.px.dev/px.vispb.Graph",
"adjacencyList": {
"fromColumn": "requestor",
"toColumn": "responder"
},
"edgeWeightColumn": "weight",
"edgeColorColumn": "max_severity",
"edgeLabelColumn": "edge_kind",
"edgeThresholds": {
"mediumThreshold": 3,
"highThreshold": 4
},
"edgeHoverInfo": [
"edge_kind",
"condition",
"criteria",
"weight",
"max_severity",
"confidence",
"num_findings",
"investigation_id"
],
"edgeLength": 500
}
},
{
"name": "Edges",
"position": {"x": 0, "y": 5, "w": 12, "h": 4},
"globalFuncOutputName": "dx_graph",
"displaySpec": {
"@type": "types.px.dev/px.vispb.Table"
}
}
]
}
6 changes: 6 additions & 0 deletions src/ui/src/containers/live-widgets/graph/graph-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ export function getGraphOptions(theme: Theme, edgeLength: number): Options {
smooth: false,
scaling: {
max: 5,
label: false,
},
font: {
strokeWidth: 0,
color: theme.palette.text.primary,
face: 'Roboto',
},
arrows: {
to: {
Expand Down
Loading
Loading