Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9a16426
feat: enhance search with pagination, fuzziness control, highlights, …
ravisuhag Mar 27, 2026
27b0647
feat: add soft deletion and hard deletion lifecycle for assets
ravisuhag Mar 27, 2026
b786c04
fix: dedupe owners, fix version sort order, and set timestamps before…
ravisuhag Mar 27, 2026
4352652
feat: add query, metric, and experiment asset types
ravisuhag Mar 27, 2026
7359082
fix: order probes by timestamp desc and add probe table indexes
ravisuhag Mar 27, 2026
8f1a1c7
feat: add WithAttributes flag to lineage query for performance optimi…
ravisuhag Mar 27, 2026
c1efff7
refactor: remove statsd instrumentation
ravisuhag Mar 27, 2026
cc3d5fd
feat: update proto with search flags, lineage options, update_only, i…
ravisuhag Mar 27, 2026
c149106
feat: implement GroupAssets API with ES composite aggregation
ravisuhag Mar 27, 2026
c978c2a
feat: add OpenTelemetry instrumentation with OTLP trace and metric ex…
ravisuhag Mar 27, 2026
2cf5bb2
build: update PROTON_COMMIT to merged main (409f146)
ravisuhag Mar 27, 2026
43c5b60
fix: remove unused const to fix golangci-lint
ravisuhag Mar 27, 2026
83c6dc6
chore: add golangci-lint config and fix pre-existing lint issues
ravisuhag Mar 27, 2026
6aae72c
fix: update golangci-lint to v2 in CI and config
ravisuhag Mar 27, 2026
9492357
build: upgrade all CI actions to latest major versions
ravisuhag Mar 27, 2026
c5d3511
fix: use golangci-lint-action v7 for golangci-lint v2 support
ravisuhag Mar 27, 2026
7ddbd3b
build: upgrade all CI actions to latest major versions
ravisuhag Mar 27, 2026
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
8 changes: 4 additions & 4 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ jobs:
golangci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v4
- uses: actions/setup-go@v6
with:
go-version: "1.20"
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v9
with:
version: v1.53
version: v2.1
args: --timeout=5m
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v6
with:
go-version: "1.20"
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v4
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Publish dev image
id: docker_dev_build
uses: docker/build-push-action@v2
uses: docker/build-push-action@v7
with:
push: true
file: "./Dockerfile.dev"
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v6
with:
go-version: "1.20"
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v4
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v4
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: latest
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ jobs:
env:
ES_TEST_SERVER_URL: "http://elasticsearch:9200"
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: "1.20"
- name: Install dependencies
Expand Down
22 changes: 22 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version: "2"

linters:
enable:
- errcheck
- staticcheck
settings:
errcheck:
exclude-functions:
- (io.Closer).Close
- (*database/sql.Conn).Close
- (*github.com/jmoiron/sqlx.Conn).Close
- (*google.golang.org/grpc.ClientConn).Close
- (github.com/ory/dockertest/v3.Resource).Close
exclusions:
rules:
- linters:
- errcheck
text: '\.Close'
- linters:
- staticcheck
text: 'QF1008'
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ COMMIT := $(shell git rev-parse --short HEAD)
TAG := "$(shell git rev-list --tags --max-count=1)"
VERSION := "$(shell git describe --tags ${TAG})-next"
BUILD_DIR=dist
PROTON_COMMIT := "ccbf219312db35a934361ebad895cb40145ca235"
PROTON_COMMIT := "409f146"

.PHONY: all build clean test tidy vet proto setup format generat

Expand Down
1 change: 0 additions & 1 deletion buf.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ plugins:
out: proto
opt:
- paths=source_relative
- lang=go
- plugin: buf.build/grpc-ecosystem/openapiv2:v2.16.0
out: proto
opt:
Expand Down
8 changes: 4 additions & 4 deletions cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
esStore "github.com/raystack/compass/internal/store/elasticsearch"
"github.com/raystack/compass/internal/store/postgres"
"github.com/raystack/compass/pkg/metrics"
"github.com/raystack/compass/pkg/statsd"
"github.com/raystack/compass/pkg/telemetry"
"github.com/raystack/salt/cmdx"
"github.com/raystack/salt/config"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -79,12 +79,12 @@ type Config struct {
// Log
LogLevel string `yaml:"log_level" mapstructure:"log_level" default:"info"`

// StatsD
StatsD statsd.Config `mapstructure:"statsd"`

// NewRelic
NewRelic metrics.NewRelicConfig `mapstructure:"newrelic"`

// Telemetry
Telemetry telemetry.Config `mapstructure:"telemetry"`

// Elasticsearch
Elasticsearch esStore.Config `mapstructure:"elasticsearch"`

Expand Down
11 changes: 6 additions & 5 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
compassserver "github.com/raystack/compass/internal/server"
esStore "github.com/raystack/compass/internal/store/elasticsearch"
"github.com/raystack/compass/internal/store/postgres"
"github.com/raystack/compass/pkg/statsd"
"github.com/raystack/compass/pkg/telemetry"
"github.com/raystack/salt/log"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -100,10 +100,12 @@ func runServer(config *Config) error {
if err != nil {
return err
}
statsdReporter, err := statsd.Init(logger, config.StatsD)

otelCleanup, err := telemetry.Init(ctx, config.Telemetry, logger)
if err != nil {
return err
return fmt.Errorf("failed to initialize telemetry: %w", err)
}
defer otelCleanup()

esClient, err := initElasticsearch(logger, config.Elasticsearch)
if err != nil {
Expand Down Expand Up @@ -132,7 +134,7 @@ func runServer(config *Config) error {
if err != nil {
return fmt.Errorf("failed to create new user repository: %w", err)
}
userService := user.NewService(logger, userRepository, user.ServiceWithStatsDReporter(statsdReporter))
userService := user.NewService(logger, userRepository)

assetRepository, err := postgres.NewAssetRepository(pgClient, userRepository, 0, config.Service.Identity.ProviderDefaultName)
if err != nil {
Expand Down Expand Up @@ -168,7 +170,6 @@ func runServer(config *Config) error {
logger,
pgClient,
nrApp,
statsdReporter,
namespaceService,
assetService,
starService,
Expand Down
13 changes: 11 additions & 2 deletions core/asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package asset
//go:generate mockery --name=Repository -r --case underscore --with-expecter --structname AssetRepository --filename asset_repository.go --output=./mocks
import (
"context"
"github.com/raystack/compass/core/namespace"
"fmt"
"time"

"github.com/raystack/compass/core/user"
"github.com/r3labs/diff/v2"
"github.com/raystack/compass/core/namespace"
"github.com/raystack/compass/core/user"
)

type Repository interface {
Expand All @@ -22,6 +23,10 @@ type Repository interface {
Upsert(ctx context.Context, ns *namespace.Namespace, ast *Asset) (string, error)
DeleteByID(ctx context.Context, id string) error
DeleteByURN(ctx context.Context, urn string) error
SoftDeleteByID(ctx context.Context, id string) (string, error)
SoftDeleteByURN(ctx context.Context, urn string) (string, error)
GetCountByIsDeleted(ctx context.Context, isDeleted bool) (int, error)
HardDeleteByURNs(ctx context.Context, urns []string) error
AddProbe(ctx context.Context, ns *namespace.Namespace, assetURN string, probe *Probe) error
GetProbes(ctx context.Context, assetURN string) ([]Probe, error)
GetProbesWithFilter(ctx context.Context, flt ProbesFilter) (map[string][]Probe, error)
Expand All @@ -39,14 +44,18 @@ type Asset struct {
URL string `json:"url" diff:"url"`
Labels map[string]string `json:"labels" diff:"labels"`
Owners []user.User `json:"owners,omitempty" diff:"owners"`
IsDeleted bool `json:"is_deleted" diff:"is_deleted"`
CreatedAt time.Time `json:"created_at" diff:"-"`
UpdatedAt time.Time `json:"updated_at" diff:"-"`
RefreshedAt *time.Time `json:"refreshed_at,omitempty" diff:"-"`
Version string `json:"version" diff:"-"`
UpdatedBy user.User `json:"updated_by" diff:"-"`
Changelog diff.Changelog `json:"changelog,omitempty" diff:"-"`
Probes []Probe `json:"probes,omitempty"`
}

var ErrAssetAlreadyDeleted = fmt.Errorf("asset already deleted")

// Diff returns nil changelog with nil error if equal
// returns wrapped r3labs/diff Changelog struct with nil error if not equal
func (a *Asset) Diff(otherAsset *Asset) (diff.Changelog, error) {
Expand Down
41 changes: 40 additions & 1 deletion core/asset/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,49 @@ type DiscoveryRepository interface {
Upsert(ctx context.Context, ns *namespace.Namespace, ast *Asset) error
DeleteByID(ctx context.Context, ns *namespace.Namespace, assetID string) error
DeleteByURN(ctx context.Context, ns *namespace.Namespace, assetURN string) error
SoftDeleteByURN(ctx context.Context, ns *namespace.Namespace, assetURN string) error
Search(ctx context.Context, cfg SearchConfig) (results []SearchResult, err error)
Suggest(ctx context.Context, cfg SearchConfig) (suggestions []string, err error)
GroupAssets(ctx context.Context, cfg GroupConfig) ([]GroupResult, error)
}

// GroupConfig represents configuration for grouping assets
type GroupConfig struct {
GroupBy []string
Filters SearchFilter
IncludeFields []string
Size int
Namespace *namespace.Namespace
}

// GroupResult represents a single group of assets
type GroupResult struct {
Fields []GroupField
Assets []Asset
}

// GroupField represents a key-value pair identifying a group
type GroupField struct {
Key string
Value string
}

// SearchFilter is a filter intended to be used as a search
// criteria for operations involving asset search
type SearchFilter = map[string][]string

// SearchFlags contains flags that modify search behavior
type SearchFlags struct {
DisableFuzzy bool
EnableHighlight bool
IsColumnSearch bool
}

// SearchConfig represents a search query along
// with any corresponding filter(s)
type SearchConfig struct {
// Text to search for
Text string `validate:"required"`
Text string

// Filters specifies document level values to look for.
// Multiple values can be specified for a single key
Expand All @@ -32,12 +62,21 @@ type SearchConfig struct {
// Number of relevant results to return
MaxResults int

// Offset is the offset for search results, used for pagination
Offset int

// RankBy is a param to rank based on a specific parameter
RankBy string

// Queries is a param to search a resource based on asset's fields
Queries map[string]string

// IncludeFields specifies which fields to include in search results
IncludeFields []string

// Flags contains optional search flags
Flags SearchFlags

// Namespace under which assets are partitioned. *Required*
Namespace *namespace.Namespace `validate:"required"`
}
Expand Down
4 changes: 2 additions & 2 deletions core/asset/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ func TestSearchConfig_Validate(t *testing.T) {
wantErr: true,
},
{
name: "fail validation if text is empty",
name: "should not fail validation if text is empty but namespace is present",
fields: fields{
Text: "",
Namespace: &namespace.Namespace{},
},
wantErr: true,
wantErr: false,
},
{
name: "should not fail validation if all required fields are non empty",
Expand Down
8 changes: 8 additions & 0 deletions core/asset/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Filter struct {
QueryFields []string
Query string
Data map[string][]string
IsDeleted *bool
}

func (f *Filter) Validate() error {
Expand All @@ -32,6 +33,7 @@ type filterBuilder struct {
offset int
sortBy string
sortDirection string
isDeleted *bool
}

func NewFilterBuilder() *filterBuilder {
Expand Down Expand Up @@ -83,13 +85,19 @@ func (fb *filterBuilder) SortDirection(sortDirection string) *filterBuilder {
return fb
}

func (fb *filterBuilder) IsDeleted(isDeleted bool) *filterBuilder {
fb.isDeleted = &isDeleted
return fb
}

func (fb *filterBuilder) Build() (Filter, error) {
flt := Filter{
Size: fb.size,
Offset: fb.offset,
SortBy: fb.sortBy,
SortDirection: fb.sortDirection,
Query: fb.q,
IsDeleted: fb.isDeleted,
}

if len(fb.data) != 0 {
Expand Down
7 changes: 5 additions & 2 deletions core/asset/lineage.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@ const (
)

type LineageQuery struct {
Level int
Direction LineageDirection
Level int
Direction LineageDirection
WithAttributes bool
IncludeDeleted bool
}

//go:generate mockery --name=LineageRepository -r --case underscore --with-expecter --structname=LineageRepository --filename=lineage_repository.go --output=./mocks
type LineageRepository interface {
GetGraph(ctx context.Context, urn string, query LineageQuery) (LineageGraph, error)
Upsert(ctx context.Context, ns *namespace.Namespace, urn string, upstreams, downstreams []string) error
DeleteByURN(ctx context.Context, urn string) error
DeleteByURNs(ctx context.Context, urns []string) error
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 | 🔴 Critical

Critical: DeleteByURNs implementation path currently enables SQL injection.

This new interface method is implemented in internal/store/postgres/lineage_repository.go using string interpolation (fmt.Sprintf("source='%s' or target='%s'", urn, urn) around Line 82), which allows crafted URNs to break query boundaries. Switch to parameterized predicates via squirrel Eq/Or instead of raw string assembly.

Suggested safe query pattern (in internal/store/postgres/lineage_repository.go)
- orClauses := make([]string, 0, len(urns))
- for _, urn := range urns {
-   orClauses = append(orClauses, fmt.Sprintf("source='%s' or target='%s'", urn, urn))
- }
- whereClause := strings.Join(orClauses, " or ")
- deleteQuery, _, err := sq.Delete("lineage_graph").Where(whereClause).ToSql()
+ ors := sq.Or{}
+ for _, urn := range urns {
+   ors = append(ors, sq.Eq{"source": urn})
+   ors = append(ors, sq.Eq{"target": urn})
+ }
+ deleteQuery, args, err := sq.Delete("lineage_graph").Where(ors).ToSql()
  if err != nil {
    return fmt.Errorf("error building delete query: %w", err)
  }
- _, err = repo.client.ExecContext(ctx, deleteQuery)
+ _, err = repo.client.ExecContext(ctx, deleteQuery, args...)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/asset/lineage.go` at line 37, The DeleteByURNs implementation currently
builds SQL with fmt.Sprintf and is vulnerable to injection; replace the
string-interpolation predicate in the DeleteByURNs method with parameterized
predicates using squirrel's Eq/Or (e.g. build a slice of predicates like
sq.Or{sq.Eq{"source": urn}, sq.Eq{"target": urn}} for each urn or accumulate
with sq.Or across urns) and pass that predicate into the repository delete/query
builder instead of concatenating strings; locate the DeleteByURNs implementation
in lineage_repository.go and remove use of fmt.Sprintf(...) so all urn values
are bound via squirrel parameters (Eq/Or) and executed safely.

}

type LineageGraph []LineageEdge
Expand Down
Loading
Loading