Skip to content

Commit 77b4770

Browse files
committed
fix: ignore linked automatic baselines
1 parent 1c492be commit 77b4770

6 files changed

Lines changed: 57 additions & 24 deletions

File tree

dev-docs/ARCHITECTURE.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,12 @@ projects. Discovery happens during normal target preparation: scan and explain
220220
read the materialized project tree, including repositories cloned through
221221
`--url`, while Git diff independently reads the base and head trees. A detected
222222
baseline is logged with its path, entry count, selection mode, and target kind;
223-
automatic discovery rejects a symbolic-link `.bomly` directory or baseline
224-
file so repository content cannot redirect the read outside the materialized
225-
target. Explicit baseline paths remain trusted user-selected inputs and may
226-
refer outside the project or through a symbolic link. Each evaluation logs
223+
automatic discovery warns and behaves as though no baseline exists when path
224+
inspection finds a symbolic-link `.bomly` directory or baseline file. This
225+
rejects discovered links but cannot prevent another process from replacing a
226+
path between inspection and reading. Explicit baseline paths remain trusted
227+
user-selected inputs and may refer outside the project or through a symbolic
228+
link. Each evaluation logs
227229
findings evaluated and accepted. Output receives ordinary findings whose policy
228230
status may be `suppressed` through `Finding.PolicyStatus` / `policy_status`, and
229231
no baseline-specific output model or pipeline stage exists. Renaming the

docs/AUDITORS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,10 @@ A project may commit `.bomly/baseline.json` to suppress accepted package
163163
findings without removing them from reports. Policy-status resolution is part of
164164
auditing: auditors first emit ordinary findings, then the audit stage marks
165165
compatible entries `suppressed`. It never removes a finding or suppresses
166-
pipeline diagnostics. Automatic discovery rejects symbolic links at the
167-
conventional baseline path; an explicit `--baseline <path>` remains a
168-
trusted user-selected path. See [Finding Baselines](BASELINES.md).
166+
pipeline diagnostics. If automatic discovery finds a symbolic link at the
167+
conventional baseline path, Bomly warns and behaves as though no baseline
168+
exists. An explicit `--baseline <path>` remains a trusted user-selected
169+
path. See [Finding Baselines](BASELINES.md).
169170

170171
## See also
171172

docs/BASELINES.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,12 @@ with its path, entry count, selection mode, and target kind. Audit completion
4949
logs include the baseline path, entries, findings evaluated, and findings
5050
accepted, including when those counts are zero.
5151

52-
For automatic discovery, `.bomly` and `baseline.json` must not be symbolic
53-
links. Bomly rejects links so repository content cannot redirect the automatic
54-
read outside the project. An explicit `--baseline <path>` is different: it is a
55-
trusted path selected by the user and may refer to a symbolic link or a file
56-
outside the project.
52+
If automatic discovery sees a symbolic-link `.bomly` directory or
53+
`baseline.json`, Bomly warns and behaves as though no project baseline exists.
54+
This rejects links discovered during path inspection; it does not protect
55+
against another process replacing a path between inspection and reading. An
56+
explicit `--baseline <path>` is different: it is a trusted path selected by the
57+
user and may refer to a symbolic link or a file outside the project.
5758

5859
Container targets have no reliable project root, so they require an absolute
5960
baseline path. For a standalone SBOM file, automatic discovery starts beside

internal/baseline/baseline.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const (
2424
DefaultRelativePath = ".bomly/baseline.json"
2525
)
2626

27+
var errAutomaticBaselineSymlink = errors.New("automatic baseline path uses a symbolic link")
28+
2729
// Document is a portable collection of package-specific finding entries.
2830
type Document struct {
2931
SchemaVersion string `json:"schema_version"`
@@ -206,6 +208,13 @@ func ResolversForTarget(selection string, target sdk.ExecutionTarget, logger *za
206208
}
207209
if automatic {
208210
if err := validateAutomaticPath(target, path); err != nil {
211+
if errors.Is(err, errAutomaticBaselineSymlink) {
212+
logger.Warn("baseline: ignored linked project policy",
213+
zap.String("path", path),
214+
zap.String("target_kind", string(target.Kind)),
215+
zap.Error(err))
216+
return LoadResult{}, nil
217+
}
209218
return LoadResult{}, err
210219
}
211220
}
@@ -248,6 +257,8 @@ func validateAutomaticPath(target sdk.ExecutionTarget, path string) error {
248257
if err != nil {
249258
return fmt.Errorf("check automatic baseline path %q: %w", path, err)
250259
}
260+
// ResolvePath currently builds this path from a constant relative name.
261+
// Keep the escape check as defense in depth if path construction changes.
251262
if relative == ".." || strings.HasPrefix(relative, ".."+string(os.PathSeparator)) || filepath.IsAbs(relative) {
252263
return fmt.Errorf("automatic baseline path %q escapes the project root", path)
253264
}
@@ -260,14 +271,17 @@ func validateAutomaticPath(target sdk.ExecutionTarget, path string) error {
260271
current = filepath.Join(current, component)
261272
info, err := os.Lstat(current)
262273
if errors.Is(err, os.ErrNotExist) {
274+
// A dangling link is returned by Lstat and handled below. ErrNotExist
275+
// therefore means a normal path component is absent.
263276
return nil
264277
}
265278
if err != nil {
266279
return fmt.Errorf("inspect automatic baseline path %q: %w", current, err)
267280
}
268281
if info.Mode()&os.ModeSymlink != 0 {
269282
return fmt.Errorf(
270-
"automatic baseline path %q uses symbolic link %q; use --baseline none to disable it or --baseline <path> to select a trusted file",
283+
"%w: %q resolves through %q",
284+
errAutomaticBaselineSymlink,
271285
path,
272286
current,
273287
)

internal/baseline/baseline_test.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ func TestResolversForTargetHandlesOptionalRequiredAndURLPolicies(t *testing.T) {
273273
}
274274
}
275275

276-
func TestResolversForTargetRejectsSymlinksOnlyForAutomaticSelection(t *testing.T) {
276+
func TestResolversForTargetIgnoresAutomaticSymlinksAndAllowsExplicitSelection(t *testing.T) {
277277
if runtime.GOOS == "windows" {
278278
t.Skip("symlink creation requires elevated privileges on Windows")
279279
}
@@ -302,11 +302,21 @@ func TestResolversForTargetRejectsSymlinksOnlyForAutomaticSelection(t *testing.T
302302
}
303303
target := sdk.ExecutionTarget{Kind: targetKind, Location: root}
304304

305-
if _, err := ResolversForTarget("auto", target, nil); err == nil ||
306-
!strings.Contains(err.Error(), "uses symbolic link") {
307-
t.Fatalf("automatic symlink error = %v", err)
305+
core, logs := observer.New(zap.WarnLevel)
306+
result, err := ResolversForTarget("auto", target, zap.New(core))
307+
if err != nil || len(result.Resolvers) != 0 || result.Path != "" {
308+
t.Fatalf("automatic symlink result = %#v, %v", result, err)
308309
}
309-
result, err := ResolversForTarget(link, target, nil)
310+
warnings := logs.FilterMessage("baseline: ignored linked project policy")
311+
if warnings.Len() != 1 {
312+
t.Fatalf("automatic symlink warnings = %#v", logs.All())
313+
}
314+
fields := warnings.All()[0].ContextMap()
315+
if fields["path"] != link || fields["target_kind"] != string(targetKind) ||
316+
!strings.Contains(fmt.Sprint(fields["error"]), "resolves through") {
317+
t.Fatalf("automatic symlink warning fields = %#v", fields)
318+
}
319+
result, err = ResolversForTarget(link, target, nil)
310320
if err != nil || len(result.Resolvers) != 1 || result.Automatic {
311321
t.Fatalf("explicit trusted symlink = %#v, %v", result, err)
312322
}
@@ -323,9 +333,13 @@ func TestResolversForTargetRejectsSymlinksOnlyForAutomaticSelection(t *testing.T
323333
}
324334
target := sdk.ExecutionTarget{Kind: targetKind, Location: root}
325335

326-
if _, err := ResolversForTarget("", target, nil); err == nil ||
327-
!strings.Contains(err.Error(), "uses symbolic link") {
328-
t.Fatalf("automatic directory symlink error = %v", err)
336+
core, logs := observer.New(zap.WarnLevel)
337+
result, err := ResolversForTarget("", target, zap.New(core))
338+
if err != nil || len(result.Resolvers) != 0 || result.Path != "" {
339+
t.Fatalf("automatic directory symlink result = %#v, %v", result, err)
340+
}
341+
if logs.FilterMessage("baseline: ignored linked project policy").Len() != 1 {
342+
t.Fatalf("automatic directory symlink warnings = %#v", logs.All())
329343
}
330344
})
331345
}

internal/support/component_docs.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -504,9 +504,10 @@ A project may commit `+"`.bomly/baseline.json`"+` to suppress accepted package
504504
findings without removing them from reports. Policy-status resolution is part of
505505
auditing: auditors first emit ordinary findings, then the audit stage marks
506506
compatible entries `+"`suppressed`"+`. It never removes a finding or suppresses
507-
pipeline diagnostics. Automatic discovery rejects symbolic links at the
508-
conventional baseline path; an explicit `+"`--baseline <path>`"+` remains a
509-
trusted user-selected path. See [Finding Baselines](BASELINES.md).
507+
pipeline diagnostics. If automatic discovery finds a symbolic link at the
508+
conventional baseline path, Bomly warns and behaves as though no baseline
509+
exists. An explicit `+"`--baseline <path>`"+` remains a trusted user-selected
510+
path. See [Finding Baselines](BASELINES.md).
510511
511512
## See also
512513

0 commit comments

Comments
 (0)