jobgraph

package
v2.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 8, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

Documentation

Overview

Package jobgraph models a workflow's jobs as a directed acyclic graph and propagates taint across it.

Existing analysis in Flowlyt is per-step or per-job. That misses a real and exploited attack shape: one job writes attacker-controlled data into a job output, and a second, more privileged job consumes it and executes it. Neither job looks dangerous on its own — the vulnerability exists only in the edge between them.

Jobs form a DAG through `needs:`, and data flows only from a dependency to its dependents. Processing jobs in topological order therefore guarantees that every input to a job is fully resolved before the job is examined, so a single pass suffices and no iteration to a fixpoint is required. That is the property this package is built around.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ExpressionsIn

func ExpressionsIn(text string) []string

ExpressionsIn returns the inner text of every ${{ }} expression, for callers that need to inspect them individually.

func HasUntrustedExpression

func HasUntrustedExpression(text string) bool

HasUntrustedExpression reports whether a text interpolates an attacker-controllable context. Exported for reuse by rules.

Types

type Analyzer

type Analyzer struct {
	// contains filtered or unexported fields
}

Analyzer performs cross-job taint propagation over a job graph.

func NewAnalyzer

func NewAnalyzer(g *Graph) *Analyzer

NewAnalyzer prepares an analyzer for a workflow's job graph.

func (*Analyzer) Analyze

func (a *Analyzer) Analyze() []Flow

Analyze propagates taint across the graph and returns the flows that reach a dangerous sink in a different job from where they originated.

Complexity is O(V + E + T), where T is the total size of the expressions and scripts in the workflow: each job is visited once, in topological order.

type Flow

type Flow struct {
	// Origin is where the data became untrusted.
	Origin Origin
	// Carrier is the job output that transported it across the boundary.
	Carrier Slot
	// SinkJob and SinkStep are where it was consumed.
	SinkJob  string
	SinkStep string
	// SinkKind describes how it was consumed, e.g. "run script".
	SinkKind string
	// SinkExpression is the reference that consumed it, e.g.
	// needs.build.outputs.version.
	SinkExpression string
	// Path is the job chain from origin to sink, for explaining the finding.
	Path []string
}

Flow is one confirmed path from an untrusted source to a dangerous sink that crosses a job boundary.

type Graph

type Graph struct {
	// Nodes is keyed by job ID.
	Nodes map[string]*Node
	// contains filtered or unexported fields
}

Graph is the job dependency DAG for a single workflow.

func Build

func Build(workflow parser.Workflow) *Graph

Build constructs the dependency graph for a workflow.

A `needs:` entry naming a job that does not exist is ignored rather than treated as an error: the workflow is invalid, but the remaining edges are still worth analysing.

Runs in O(V + E).

func (*Graph) Ancestors

func (g *Graph) Ancestors(id string) []string

Ancestors returns every job that the given job transitively depends on.

Used to explain a finding: the path from where data became untrusted to where it was executed.

func (*Graph) Cyclic

func (g *Graph) Cyclic() []string

Cyclic returns the job IDs that participate in a dependency cycle, if any.

func (*Graph) String

func (g *Graph) String() string

String renders the graph in dependency order, for debugging.

func (*Graph) TopologicalOrder

func (g *Graph) TopologicalOrder() []string

TopologicalOrder returns job IDs ordered so that every job appears after all of its dependencies.

Jobs involved in a dependency cycle are excluded; use Cyclic to retrieve them.

type Node

type Node struct {
	ID  string
	Job parser.Job

	// Needs are the job IDs this job depends on, in declaration order.
	Needs []string
	// Dependents are the job IDs that depend on this job.
	Dependents []string
}

Node is one job in the graph.

type Origin

type Origin struct {
	// JobID and StepName locate the step that introduced the taint.
	JobID    string
	StepName string
	// Expression is the untrusted expression, e.g. github.event.issue.title.
	Expression string
}

Origin records where a tainted value entered the workflow.

type Slot

type Slot struct {
	Kind   string // "step" or "job"
	JobID  string
	Name   string // step output name, or job output name
	StepID string // set for Kind == "step"
}

Slot identifies a value that can carry taint between steps or jobs.

func (Slot) String

func (s Slot) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL