Documentation
¶
Overview ¶
Package analyze produces the `vulnetix analyze` report: a repository's tech-stack graph, the cross-repo join keys other repositories match against, and the metrics for that repository — each metric carrying the evidence that produced it.
Index ¶
- Constants
- func CompiledReportSchema() (*jsonschema.Schema, error)
- func ToWire(r *Report) (vdb.CliInsightsRequest, error)
- func ValidateReport(body []byte) error
- type Affiliation
- type Alias
- type Attachments
- type BranchRecord
- type Builder
- func (b *Builder) AddFile(rec *FileRecord) EvidenceRef
- func (b *Builder) AddRecord(id string, rec any) EvidenceRef
- func (b *Builder) Assertion(m Metric, value any, refs []EvidenceRef)
- func (b *Builder) Collected(c Collector)
- func (b *Builder) Count(m Metric, refs []EvidenceRef)
- func (b *Builder) CountTruncated(m Metric, refs []EvidenceRef, omitted int, reason string)
- func (b *Builder) Diagnose(d Diagnostic)
- func (b *Builder) Finish(completedAt time.Time) (*Report, []byte, error)
- func (b *Builder) Has(id string) bool
- func (b *Builder) SetGraph(g *Graph)
- func (b *Builder) SetSARIF(doc any)
- func (b *Builder) SetWindow(w *Window)
- func (b *Builder) Statistic(m Metric, value float64, refs []EvidenceRef)
- func (b *Builder) Unmeasured(m Metric, reason string)
- type Classification
- type Collector
- type CommitRecord
- type ContributorRecord
- type CrossRepoEdge
- type DependencyRecord
- type Diagnostic
- type Durations
- type Edge
- type EnrichFunc
- type Evidence
- type EvidenceRef
- type FileRecord
- type Graph
- type GraphElementRecord
- type GraphTruncation
- type Identity
- type IdentitySet
- type IssueRecord
- type Metric
- type MetricWindow
- type Node
- type Options
- type Ownership
- type PackageInsight
- type Preflight
- type PullRequestRecord
- type Reference
- type Report
- type Reporter
- type ReviewRecord
- type RunMeta
- type Signature
- type Target
- type Tool
- type VersionStamp
- type Window
Constants ¶
const ReportSchemaID = "https://vulnetix.com/schemas/vulnetix-analyze-report.schema.json"
ReportSchemaID is the $id of the report schema, and the URL it is published at.
const SchemaVersion = "1.0.0"
const (
TotalSteps = stepReport
)
The steps, in the order Run actually executes them.
The order matters, and it is the order of execution rather than any tidier grouping: a step number that is lower than the one before it makes the bar run backwards, which reads as a bug in the tool rather than a bug in the numbering.
Variables ¶
This section is empty.
Functions ¶
func CompiledReportSchema ¶
func CompiledReportSchema() (*jsonschema.Schema, error)
CompiledReportSchema compiles the report schema together with every open schema it extends. The vendored schemas span three JSON Schema drafts (SARIF is draft-04, CycloneDX and SPDX draft-07, OpenVEX and ours 2020-12); each declares its own $schema and is resolved under its own dialect, so the mix is deliberate and must not be flattened.
func ToWire ¶ added in v3.59.3
func ToWire(r *Report) (vdb.CliInsightsRequest, error)
ToWire turns a report into an insights submission.
func ValidateReport ¶
ValidateReport checks a marshalled report against the schema, and then against the one rule the schema cannot express.
JSON Schema can require that a metric declares how its evidence relates to its value; it cannot check that the declaration is true, because that means comparing the length of one field against the value of a sibling. That check — a metric of 23 references 23 evidence items — is the whole point of the format, so it is enforced here and every report passes through it.
Types ¶
type Affiliation ¶
type Attachments ¶
type BranchRecord ¶
type BranchRecord struct {
ID string `json:"id"`
Type string `json:"type"` // "branch"
Name string `json:"name"`
IsDefault bool `json:"isDefault,omitempty"`
Merged bool `json:"merged,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
LastCommitAt string `json:"lastCommitAt,omitempty"`
AgeSeconds *int `json:"ageSeconds,omitempty"`
}
type Builder ¶
type Builder struct {
// contains filtered or unexported fields
}
Builder assembles a report and — this is the point of it — makes the evidence invariant impossible to break by accident.
A collector never states a count. It hands over the evidence and the builder derives the number from it. There is no code path where a collector can say "23" and attach 22 things, because there is no code path where a collector says a number at all.
func (*Builder) AddFile ¶ added in v3.59.3
func (b *Builder) AddFile(rec *FileRecord) EvidenceRef
AddFile stores a file record — or folds it into the record already held for that path — and returns a ref to whichever it is. Collectors use this rather than AddRecord for files: the file walker and the policy checks both have something to say about `LICENSE`, and they have to end up saying it about the same record.
func (*Builder) AddRecord ¶
func (b *Builder) AddRecord(id string, rec any) EvidenceRef
AddRecord stores an evidence record and returns a ref to it. The record's id must be unique; a duplicate is a programming error, not a data condition, so it panics rather than silently overwriting evidence that something else is already pointing at.
func (*Builder) Assertion ¶
func (b *Builder) Assertion(m Metric, value any, refs []EvidenceRef)
Assertion adds a judgement — a boolean, a category — supported by evidence that has no countable relationship to it.
func (*Builder) Count ¶
func (b *Builder) Count(m Metric, refs []EvidenceRef)
Count adds a metric whose value IS the number of evidence items. The caller cannot get the count wrong because the caller does not supply it.
func (*Builder) CountTruncated ¶
func (b *Builder) CountTruncated(m Metric, refs []EvidenceRef, omitted int, reason string)
CountTruncated adds a count metric that hit a cap. The total is present + omitted, and the reason is mandatory — a cap that was hit silently is the failure mode this whole format exists to prevent, so there is no way to express one.
func (*Builder) Diagnose ¶
func (b *Builder) Diagnose(d Diagnostic)
func (*Builder) Finish ¶
Finish seals the report and validates it against the schema and the evidence invariant. Nothing is written or uploaded that has not been through this: a report we would reject on the way in is a report we must not produce on the way out.
func (*Builder) Has ¶ added in v3.59.1
Has reports whether a metric has already been emitted. Used by a collector that partially succeeded, so the failure path can fill in what is missing without emitting anything twice.
func (*Builder) Statistic ¶
func (b *Builder) Statistic(m Metric, value float64, refs []EvidenceRef)
Statistic adds a metric that summarises a population — a median, a mean, a slope, a ratio. The evidence is the whole population it was computed over, because a statistic without its distribution is a number you cannot argue with.
func (*Builder) Unmeasured ¶
Unmeasured records a metric that could not be computed, and why.
This is the difference between "we found no unreviewed commits" and "we could not check whether commits were reviewed", and it is a difference every tool surveyed manages to lose. The value is null, never zero, and the reason lands in the diagnostics attached to that metric id.
type Classification ¶
type CommitRecord ¶
type CommitRecord struct {
ID string `json:"id"`
Type string `json:"type"` // "commit"
SHA string `json:"sha"`
URL string `json:"url,omitempty"`
Message string `json:"message,omitempty"`
AuthoredAt string `json:"authoredAt,omitempty"`
CommittedAt string `json:"committedAt"`
Author *Identity `json:"author,omitempty"`
Committer *Identity `json:"committer,omitempty"`
CoAuthors []Identity `json:"coAuthors,omitempty"`
ParentCount int `json:"parentCount,omitempty"`
Signature *Signature `json:"signature,omitempty"`
FilesChanged int `json:"filesChanged,omitempty"`
Insertions int `json:"insertions,omitempty"`
Deletions int `json:"deletions,omitempty"`
Paths []string `json:"paths,omitempty"`
CycleTimeSeconds *int `json:"cycleTimeSeconds,omitempty"`
}
type ContributorRecord ¶
type ContributorRecord struct {
ID string `json:"id"`
Type string `json:"type"` // "contributor"
Identity *Identity `json:"identity"`
Aliases []Alias `json:"aliases,omitempty"`
FirstSeenAt string `json:"firstSeenAt,omitempty"`
LastSeenAt string `json:"lastSeenAt,omitempty"`
Commits int `json:"commits,omitempty"`
CommitsInWindow int `json:"commitsInWindow,omitempty"`
Insertions int `json:"insertions,omitempty"`
Deletions int `json:"deletions,omitempty"`
FilesTouched int `json:"filesTouched,omitempty"`
TenureSeconds *int `json:"tenureSeconds,omitempty"`
Status string `json:"status,omitempty"`
}
type CrossRepoEdge ¶
type CrossRepoEdge struct {
ID string `json:"id"`
LocalNodeID string `json:"localNodeId"`
JoinKind string `json:"joinKind"`
JoinKey string `json:"joinKey"`
Role string `json:"role"` // provides | consumes
TargetRepoHint string `json:"targetRepoHint,omitempty"`
Confidence float64 `json:"confidence,omitempty"`
EvidenceRef *EvidenceRef `json:"evidenceRef,omitempty"`
Properties map[string]any `json:"properties,omitempty"`
}
CrossRepoEdge is a declared intent to join, not a resolved edge. The scanner never reads another repository — it publishes a normalised key and says whether this repo provides it or consumes it. The org graph forms server-side where one repo's provides meets another's consumes. This is the whole reason a single-repo scan can build an org-wide graph.
type DependencyRecord ¶
type DependencyRecord struct {
ID string `json:"id"`
Type string `json:"type"` // "dependency"
Purl string `json:"purl"`
BomRef string `json:"bomRef,omitempty"`
Ecosystem string `json:"ecosystem,omitempty"`
ManifestPath string `json:"manifestPath,omitempty"`
Scope string `json:"scope,omitempty"`
DiscoveredVia string `json:"discoveredVia,omitempty"`
DeclaredVersion string `json:"declaredVersion,omitempty"`
ResolvedVersion string `json:"resolvedVersion,omitempty"`
LatestVersion string `json:"latestVersion,omitempty"`
VersionsBehind *int `json:"versionsBehind,omitempty"`
PublishedAt string `json:"publishedAt,omitempty"`
AgeSeconds *int `json:"ageSeconds,omitempty"`
EOL bool `json:"eol,omitempty"`
AdvisoryCount int `json:"advisoryCount,omitempty"`
}
type Diagnostic ¶
type Durations ¶
type Durations struct {
TimeToFirstResponseSeconds *int `json:"timeToFirstResponseSeconds,omitempty"`
TimeToFirstReviewSeconds *int `json:"timeToFirstReviewSeconds,omitempty"`
TimeToCloseSeconds *int `json:"timeToCloseSeconds,omitempty"`
TimeToMergeSeconds *int `json:"timeToMergeSeconds,omitempty"`
TimeToAnswerSeconds *int `json:"timeToAnswerSeconds,omitempty"`
TimeInLabelSeconds map[string]int `json:"timeInLabelSeconds,omitempty"`
// OpenEnded marks a duration measured against "now" because the item is still open. Such
// a value grows between runs and must not be compared across reports as though it were
// settled.
OpenEnded bool `json:"openEnded,omitempty"`
}
Durations are always integer seconds. issue-metrics emits `"6 days, 7:08:52"` in its JSON, which no consumer can do arithmetic on without writing a parser for a format that exists nowhere else.
type Edge ¶
type Edge struct {
ID string `json:"id"`
Kind string `json:"kind"`
From string `json:"from"`
To string `json:"to"`
Confidence float64 `json:"confidence,omitempty"`
Resolution string `json:"resolution,omitempty"`
EvidenceRef *EvidenceRef `json:"evidenceRef,omitempty"`
Properties map[string]any `json:"properties,omitempty"`
}
type EnrichFunc ¶
type EnrichFunc func(purls []string) (map[string]PackageInsight, error)
EnrichFunc fetches registry metadata for a batch of PURLs. Injected rather than called directly so the collector can be tested without a network, and so that an unauthenticated run degrades to `Unmeasured` at one obvious place instead of failing somewhere deep.
type EvidenceRef ¶
type EvidenceRef struct {
Kind string `json:"kind"`
RunIndex *int `json:"runIndex,omitempty"`
ResultIndex *int `json:"resultIndex,omitempty"`
StatementIndex *int `json:"statementIndex,omitempty"`
BomRef string `json:"bomRef,omitempty"`
SpdxID string `json:"spdxId,omitempty"`
Check string `json:"check,omitempty"`
DetailIndex *int `json:"detailIndex,omitempty"`
RecordID string `json:"recordId,omitempty"`
}
func SARIFRef ¶
func SARIFRef(resultIndex int) EvidenceRef
SARIFRef points at result i of run 0 — the single run every analyze report emits.
type FileRecord ¶
type FileRecord struct {
ID string `json:"id"`
Type string `json:"type"` // "file"
Path string `json:"path"`
Language string `json:"language,omitempty"`
Tags []string `json:"tags,omitempty"`
SizeBytes *int `json:"sizeBytes,omitempty"`
Lines *int `json:"lines,omitempty"`
Code *int `json:"code,omitempty"`
Comments *int `json:"comments,omitempty"`
Blanks *int `json:"blanks,omitempty"`
Complexity *int `json:"complexity,omitempty"`
Binary bool `json:"binary,omitempty"`
Commits int `json:"commits,omitempty"`
Authors int `json:"authors,omitempty"`
FirstSeenAt string `json:"firstSeenAt,omitempty"`
LastChangedAt string `json:"lastChangedAt,omitempty"`
Ownership *Ownership `json:"ownership,omitempty"`
}
type Graph ¶
type Graph struct {
Nodes []Node `json:"nodes,omitempty"`
Edges []Edge `json:"edges,omitempty"`
CrossRepoEdges []CrossRepoEdge `json:"crossRepoEdges,omitempty"`
Truncation *GraphTruncation `json:"truncation,omitempty"`
}
type GraphElementRecord ¶
type GraphTruncation ¶
type Identity ¶
type Identity struct {
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
Login string `json:"login,omitempty"`
CanonicalID string `json:"canonicalId,omitempty"`
IsBot bool `json:"isBot,omitempty"`
BotKind string `json:"botKind,omitempty"`
BotRule string `json:"botRule,omitempty"`
EmailKind string `json:"emailKind,omitempty"`
Affiliation *Affiliation `json:"affiliation,omitempty"`
}
func ClassifyIdentity ¶
ClassifyIdentity fills in the bot, email-kind and login fields for one git identity.
The bot cascade is ordered most-specific-first and the rule that fired is recorded, so a user who disagrees with a classification can see exactly what made it and fix the catalog rather than guess.
type IdentitySet ¶
type IdentitySet struct {
// contains filtered or unexported fields
}
IdentitySet accumulates identities and merges the ones that are the same person.
Merging is by normalised email only. That is a deliberately conservative choice: name similarity (git-intelligence merges at Levenshtein ≥ 0.85) merges "Chris Langton" with "Chris Langtry", and a wrong merge is invisible in the output while a missed merge is merely a duplicate row someone can see. Where two identities share a GitHub login parsed out of a noreply address, they are merged on that instead — that one is a fact, not a guess.
func NewIdentitySet ¶
func NewIdentitySet() *IdentitySet
func (*IdentitySet) All ¶
func (s *IdentitySet) All() []*ContributorRecord
All returns the contributors in first-seen order — deterministic output for the same input.
func (*IdentitySet) Observe ¶
func (s *IdentitySet) Observe(id Identity) *ContributorRecord
Observe records one appearance of an identity and returns the contributor it belongs to.
type IssueRecord ¶
type IssueRecord struct {
ID string `json:"id"`
Type string `json:"type"` // "issue"
URL string `json:"url"`
Number int `json:"number,omitempty"`
Title string `json:"title,omitempty"`
Author *Identity `json:"author,omitempty"`
State string `json:"state,omitempty"`
StateReason string `json:"stateReason,omitempty"`
Labels []string `json:"labels,omitempty"`
CreatedAt string `json:"createdAt"`
ClosedAt string `json:"closedAt,omitempty"`
// FirstResponseAt is the first comment by somebody other than the author, excluding bots.
// Empty means no such response exists — which is not a response time of zero.
FirstResponseAt string `json:"firstResponseAt,omitempty"`
AnsweredAt string `json:"answeredAt,omitempty"`
Durations *Durations `json:"durations,omitempty"`
}
type Metric ¶
type Metric struct {
ID string `json:"id"`
Family string `json:"family"`
Name string `json:"name"`
Definition string `json:"definition"`
Unit string `json:"unit,omitempty"`
Statistic string `json:"statistic,omitempty"`
// any, because the schema allows number | string | boolean | null and an unmeasured
// metric must stay null rather than collapsing to zero.
Value any `json:"value"`
Window *MetricWindow `json:"window,omitempty"`
Classification *Classification `json:"classification,omitempty"`
EvidenceSemantics string `json:"evidenceSemantics"`
EvidenceCompleteness string `json:"evidenceCompleteness"`
PopulationSize *int `json:"populationSize,omitempty"`
OmittedCount int `json:"omittedCount,omitempty"`
TruncationReason string `json:"truncationReason,omitempty"`
EvidenceRefs []EvidenceRef `json:"evidenceRefs"`
References []Reference `json:"references,omitempty"`
}
type MetricWindow ¶
type Node ¶
type Node struct {
ID string `json:"id"`
Kind string `json:"kind"`
Name string `json:"name"`
QualifiedName string `json:"qualifiedName,omitempty"`
Path string `json:"path,omitempty"`
StartLine int `json:"startLine,omitempty"`
EndLine int `json:"endLine,omitempty"`
Language string `json:"language,omitempty"`
Purl string `json:"purl,omitempty"`
Exported bool `json:"exported,omitempty"`
Properties map[string]any `json:"properties,omitempty"`
}
type Options ¶
type Options struct {
Path string
// WindowDays bounds the history walk. Everything derived from history is relative to it,
// which is why the window is stamped on the report and on every metric that used it.
WindowDays int
MaxCommits int
ComplexityThreshold int
NoGit bool
NoFiles bool
NoDeps bool
NoTrust bool
NoForge bool
// Silent suppresses the startup authentication line. The check still runs.
Silent bool
// Enrich fetches registry metadata for a batch of PURLs, so dependency staleness can be
// computed. Nil when the user is not authenticated to the Vulnetix API — in which case the
// staleness metrics are Unmeasured, not zero.
Enrich EnrichFunc
// Progress receives stage and step updates. Nil is valid and reports nothing — a long walk
// with no output is indistinguishable from a hang, but only a terminal needs telling.
Progress Reporter
}
func DefaultOptions ¶
func DefaultOptions() Options
type PackageInsight ¶
type PackageInsight struct {
Purl string
PublishedAt *int64
LatestVersion string
// Versions is the ecosystem's release list, newest first.
Versions []VersionStamp
// Recommended is the version the ecosystem (or Safe Harbour) says to be on.
Recommended string
IsEOL bool
}
PackageInsight is the subset of the endpoint's response this collector uses.
type Preflight ¶
type Preflight struct {
Target Target
Forge *forge.Client
Repo forge.Repo
// ForgeStatus explains what happened, and is printed unless silenced. Exactly one of
// these three things is true, and the user is told which.
ForgeStatus string
}
Preflight resolves the repository and verifies forge access, before any scanning begins.
It runs first and it can fail the command. A five-minute analysis that ends in a report where a third of the metrics are null because a token was missing is a waste of the user's time — and a report full of nulls is the kind of thing people stop reading, which then costs them the metrics that *were* measured.
type PullRequestRecord ¶
type PullRequestRecord struct {
ID string `json:"id"`
Type string `json:"type"` // "pull_request"
URL string `json:"url"`
Number int `json:"number,omitempty"`
Title string `json:"title,omitempty"`
Author *Identity `json:"author,omitempty"`
State string `json:"state,omitempty"`
CreatedAt string `json:"createdAt"`
// ReadyForReviewAt is the anchor every response and merge duration is measured from, so
// that time a pull request spent in draft is not charged against its reviewers.
ReadyForReviewAt string `json:"readyForReviewAt,omitempty"`
FirstResponseAt string `json:"firstResponseAt,omitempty"`
FirstReviewAt string `json:"firstReviewAt,omitempty"`
ClosedAt string `json:"closedAt,omitempty"`
// MergedAt is set only if the pull request was actually merged. A pull request closed
// without merging has ClosedAt and no MergedAt, and the two are never conflated.
MergedAt string `json:"mergedAt,omitempty"`
MergedBy *Identity `json:"mergedBy,omitempty"`
Additions int `json:"additions,omitempty"`
Deletions int `json:"deletions,omitempty"`
ChangedFiles int `json:"changedFiles,omitempty"`
CommentCount int `json:"commentCount,omitempty"`
// ReviewerCount counts distinct reviewers other than the author. A self-approval is not
// review coverage.
ReviewerCount int `json:"reviewerCount,omitempty"`
// ApprovalStatus: approved | changes_requested | review_required | unknown. `unknown`
// means no review could be resolved at all — distinct from `review_required`, which means
// one was required and not given.
ApprovalStatus string `json:"approvalStatus,omitempty"`
TimeInDraftSeconds *int `json:"timeInDraftSeconds,omitempty"`
Durations *Durations `json:"durations,omitempty"`
}
type Report ¶
type Report struct {
SchemaVersion string `json:"schemaVersion"`
Tool Tool `json:"tool"`
Target Target `json:"target"`
Run RunMeta `json:"run"`
Graph *Graph `json:"graph,omitempty"`
Metrics []Metric `json:"metrics"`
Evidence *Evidence `json:"evidence,omitempty"`
Attachments *Attachments `json:"attachments,omitempty"`
Diagnostics []Diagnostic `json:"diagnostics,omitempty"`
}
func Run ¶
Run produces the report. Every collector that runs, is skipped, or fails is recorded on the report — so a consumer can always tell a metric that is missing from a metric that is zero.
func RunGraphOnly ¶ added in v3.66.0
RunGraphOnly produces a schema-valid insights report whose purpose is the tech-stack graph. It is used by scanner subcommands after they persist SCA/SARIF results, so the org graph can stay fresh even when teams never run `vulnetix analyze`. It deliberately skips history, forge and trust collectors; scanner-driven graph runs must not pretend to have measured the full business/security metric set.
type Reporter ¶ added in v3.59.0
type Reporter interface {
// Stage sets what is happening now, without advancing the step count. Used for the long
// passes, where "Walking history (3,400 commits)" is the difference between a tool that is
// working and a tool that has hung.
Stage(msg string)
// Step advances to a completed step.
Step(done int, msg string)
}
Reporter receives progress updates. A nil Reporter is valid and does nothing, so every collector can call it unconditionally.
type ReviewRecord ¶
type Target ¶
type Target struct {
RepoID string `json:"repoId"`
OrgKey string `json:"orgKey,omitempty"`
RemoteURL string `json:"remoteUrl,omitempty"`
DefaultBranch string `json:"defaultBranch,omitempty"`
HeadCommit string `json:"headCommit,omitempty"`
HeadCommittedAt string `json:"headCommittedAt,omitempty"`
RootPath string `json:"rootPath,omitempty"`
}