Files
fleet/server/chart/api/chart.go
T

205 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import (
"context"
"time"
"github.com/RoaringBitmap/roaring"
)
// SampleStrategy describes how a dataset's samples combine within a bucket and
// whether rows can collapse across buckets when the bitmap is unchanged.
type SampleStrategy string
const (
// SampleStrategyAccumulate means each sample is a partial observation.
// Writes: every row is born closed (valid_to set at insert time to bucketEnd).
// Within-bucket samples OR-merge into the existing row via ODKU; a sample in
// a new bucket just creates a new row with a new valid_from. No explicit
// close step, no cross-bucket collapse.
// Reads: bucket value = OR of every row whose interval overlaps the bucket
// ("hosts observed at any point during the bucket").
// Used for datasets like uptime and software usage.
// @todo: implement job to collapse identical consecutive rows
// to optimize storage and query performance.
SampleStrategyAccumulate SampleStrategy = "accumulate"
// SampleStrategySnapshot means each sample is the full state of a single moment.
// Writes: rows are always keyed to 1h boundaries (so row transitions align
// to hour marks regardless of tz). Within a 1h write-bucket, the latest
// sample's bitmap overwrites via ODKU — last sample wins. Across buckets,
// unchanged state keeps the row open (valid_to = sentinel); a changed sample
// closes the prior row at the new hour boundary and opens a new one.
// Reads: bucket value = OR across entities of each entity's row active at
// bucketEnd ("state as of the end of the bucket"). An entity whose row was
// closed mid-bucket with no replacement is absent at bucketEnd.
// Used for datasets like CVE and software inventory.
SampleStrategySnapshot SampleStrategy = "snapshot"
)
// Dataset defines the interface for a chartable dataset.
type Dataset interface {
// Name returns the dataset identifier used in the DB and API path.
Name() string
// DefaultResolutionHours returns the default display granularity in hours.
// Used when the caller doesn't specify RequestOpts.Resolution. Unrelated
// to write-side granularity — all collectors write at 1h regardless of
// display resolution; see SampleStrategy for details.
DefaultResolutionHours() int
// SampleStrategy returns how samples combine within and across buckets.
SampleStrategy() SampleStrategy
// Collect is called by the cron job to populate data in bulk.
//
// disabledFleetIDs scopes which fleets contribute to this collection. The
// orchestrator derives it from per-team config (teams whose Enabled(name)
// is false). Implementations should use this to filter out hosts from
// disabled fleets when collecting data.
//
// No-team hosts (team_id IS NULL) are always included when the orchestrator
// invokes Collect — the orchestrator skips Collect entirely if the global
// flag is off.
Collect(ctx context.Context, store DatasetStore, now time.Time, disabledFleetIDs []uint) error
// DefaultVisualization returns the default visualization type (e.g. "line", "heatmap").
DefaultVisualization() string
}
// DatasetStore is the narrow interface that datasets need for their Collect
// method. It is satisfied by the chart internal Datastore, keeping dataset
// implementations decoupled from internals.
type DatasetStore interface {
// FindOnlineHostIDs returns host IDs that are "online right now" using a
// platform-specific predicate. Non-mobile (osquery) hosts use the product's
// standard online predicate (host_seen_times.seen_time within the host's own
// check-in interval). Mobile hosts (iOS, iPadOS, Android), which only check
// in via MDM, use their MDM activity signal (nano_enrollments.last_seen_at,
// falling back to detail_updated_at) within a fixed mobile online window.
// Used by datasets like uptime.
FindOnlineHostIDs(ctx context.Context, now time.Time, disabledFleetIDs []uint) ([]uint, error)
// AffectedHostIDsByCVE returns host IDs grouped by CVE, scoped to the given
// cves set. nil or empty cves returns an empty map — callers must pass the
// CVE set they want to collect for. Unresolved-only is implicit in the
// underlying joins: a host's software/OS row transitions when it upgrades
// past the vulnerable version, so the join naturally stops matching.
AffectedHostIDsByCVE(ctx context.Context, disabledFleetIDs []uint, cves []string) (map[string][]uint, error)
// CollectibleCVEs returns every CVE ID, at all severities, on the curated
// set of tracked software unioned with all operating-system vulnerabilities.
// Used by the CVE collector to scope collection. Display-time narrowing
// (severity, category, EPSS, etc.) happens later at read time, so the
// collector deliberately records the wide set. See the mysql implementation.
CollectibleCVEs(ctx context.Context) ([]string, error)
// RecordBucketData writes one or more entity bitmaps for the given bucket
// using the specified sample strategy. See SampleStrategy for semantics.
// Bitmaps are passed in op form (*roaring.Bitmap); the datastore
// serializes via chart.BitmapToBlob at the storage boundary.
RecordBucketData(
ctx context.Context,
dataset string,
bucketStart time.Time,
bucketSize time.Duration,
strategy SampleStrategy,
entityBitmaps map[string]*roaring.Bitmap,
) error
}
// MetricCVE is the metric name of the vulnerability-exposure (CVE) dataset.
// The CVE entity filters apply only to this metric.
const MetricCVE = "cve"
// CVE chart software category keys. These are the API contract for the
// `software_filters` query parameter and are mirrored by the frontend. The
// "os" category covers both operating-system vulnerabilities and the kernel
// software matchers.
const (
CVECategoryOS = "os"
CVECategoryBrowsers = "browsers"
CVECategoryOffice = "office"
CVECategoryAdobe = "adobe"
)
// Host is a minimal host type for authorization checks within the chart bounded context.
// The JSON tags matter: the OPA rego policy reads object.team_id via the JSON-encoded
// input, so renaming or dropping the tag silently breaks team-scoped authorization.
type Host struct {
ID uint `json:"id"`
TeamID *uint `json:"team_id"`
}
// AuthzType implements platform_authz.AuthzTyper.
func (h *Host) AuthzType() string { return "host" }
// DataPoint represents a single data point in the chart response.
type DataPoint struct {
Timestamp time.Time `json:"timestamp"`
Value int `json:"value"`
}
// Response is the API response for chart data.
type Response struct {
Metric string `json:"metric"`
Visualization string `json:"visualization"`
TotalHosts int `json:"total_hosts"`
Resolution string `json:"resolution"`
Days int `json:"days"`
Filters Filters `json:"filters"`
Data []DataPoint `json:"data"`
}
// RequestOpts captures the parsed query parameters for a chart request.
type RequestOpts struct {
Days int
// Resolution is the display granularity in hours. Must be 0 or a positive
// divisor of 24. 0 means "use the dataset's default resolution."
Resolution int
// TZOffsetMinutes is the client's UTC offset as reported by JavaScript's
// Date.getTimezoneOffset() (positive = west of UTC, e.g. CDT = 300).
// Used to align hourly bucket boundaries to local time.
TZOffsetMinutes int
// TeamID scopes the request to a single team. nil = global (authz + data
// both fall back to the user's accessible scope). *TeamID == 0 means
// hosts with no team assignment, matching Fleet's convention elsewhere.
TeamID *uint
LabelIDs []uint
Platforms []string
IncludeHostIDs []uint
ExcludeHostIDs []uint
// CVE entity filters (apply only to the MetricCVE metric).
SoftwareFilters []string
KnownExploit bool
// EPSS bounds are 0.01.0 (matching cve_meta.epss_probability); nil means
// no bound. The frontend converts its 0100 % input before sending.
EPSSMin *float64
EPSSMax *float64
// Severity (CVSS) bounds are accepted but ignored this round — the service
// forces critical-only [9.0, 10.0]. See the severity TODO in the service.
SeverityMin *float64
SeverityMax *float64
// ExcludeCVEs is a subtractive filter — these CVEs are removed from the
// resolved entity set.
ExcludeCVEs []string
}
// Filters captures the applied filters for a chart request.
type Filters struct {
TeamID *uint `json:"fleet_id,omitempty"`
LabelIDs []uint `json:"label_ids,omitempty"`
Platforms []string `json:"platforms,omitempty"`
IncludeHostIDs []uint `json:"include_host_ids,omitempty"`
ExcludeHostIDs []uint `json:"exclude_host_ids,omitempty"`
SoftwareFilters []string `json:"software_filters,omitempty"`
KnownExploit bool `json:"has_known_exploit,omitempty"`
EPSSMin *float64 `json:"epss_min,omitempty"`
EPSSMax *float64 `json:"epss_max,omitempty"`
SeverityMin *float64 `json:"severity_min,omitempty"`
SeverityMax *float64 `json:"severity_max,omitempty"`
ExcludeCVEs []string `json:"exclude_vulnerabilities,omitempty"`
}