bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
126 lines
4.0 KiB
Go
126 lines
4.0 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package apps
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// isSensitiveRelPath reports whether a relative path inside the candidate
|
|
// manifest is a well-known env / credential file that should not ship to a
|
|
// public-internet share URL. The check is path-element-wise (each
|
|
// "/"-delimited segment is inspected) so credential files nested under
|
|
// arbitrary subdirectories are still caught.
|
|
//
|
|
// Used by +html-publish: dry-run AND Execute both block by default when any
|
|
// candidate matches. Pass --allow-sensitive to override (legitimate cases:
|
|
// a documentation site shipping example credential files on purpose).
|
|
//
|
|
// Scope is intentionally narrow — only files that conventionally hold API
|
|
// tokens or service credentials, not the broader "anything cryptographic"
|
|
// surface. SSH private keys, generic *.pem / *.key, and SCM internals are
|
|
// out of scope here; if they leak it's a separate problem to address.
|
|
func isSensitiveRelPath(rel string) bool {
|
|
if rel == "" {
|
|
return false
|
|
}
|
|
parts := strings.Split(rel, "/")
|
|
for i, p := range parts {
|
|
switch {
|
|
case p == ".env" || strings.HasPrefix(p, ".env."):
|
|
return true
|
|
case p == ".npmrc":
|
|
return true
|
|
case p == ".netrc":
|
|
return true
|
|
case p == ".git-credentials":
|
|
return true
|
|
}
|
|
if i == 0 {
|
|
continue
|
|
}
|
|
parent := parts[i-1]
|
|
switch parent {
|
|
case ".aws":
|
|
if p == "credentials" {
|
|
return true
|
|
}
|
|
case ".docker":
|
|
if p == "config.json" {
|
|
return true
|
|
}
|
|
case ".kube":
|
|
if p == "config" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasParentAnchoredCredentialPair scans a "/"-delimited path for the
|
|
// cloud-SDK matchers that depend on a conventional parent dir:
|
|
// .aws/credentials, .docker/config.json, .kube/config. The leaf-name
|
|
// matchers (.env / .npmrc / ...) intentionally do NOT run here, so callers
|
|
// can probe a path that includes surrounding root context without risking
|
|
// a leaf-rule false-positive on the context segment itself (e.g. a literal
|
|
// ".env" directory somewhere in --path's ancestry).
|
|
func hasParentAnchoredCredentialPair(path string) bool {
|
|
parts := strings.Split(path, "/")
|
|
for i := 1; i < len(parts); i++ {
|
|
switch parts[i-1] {
|
|
case ".aws":
|
|
if parts[i] == "credentials" {
|
|
return true
|
|
}
|
|
case ".docker":
|
|
if parts[i] == "config.json" {
|
|
return true
|
|
}
|
|
case ".kube":
|
|
if parts[i] == "config" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isSensitiveCandidate is the call-site wrapper used by +html-publish.
|
|
//
|
|
// Two passes:
|
|
//
|
|
// 1. Scan RelPath with the full matcher (isSensitiveRelPath). Handles the
|
|
// common in-tree case (e.g. ./site/.env, ./dist/.docker/config.json).
|
|
// 2. Re-probe at the boundary between rootPath and the candidate, using
|
|
// ONLY hasParentAnchoredCredentialPair. walker strips the root segment
|
|
// via filepath.Rel, so when --path is itself the conventional parent
|
|
// dir (e.g. ./.aws) RelPath comes back as a bare "credentials" and
|
|
// step 1 has no parent to anchor on. Re-prepending the root's basename
|
|
// — or, for the single-file form, the parent dir's basename of
|
|
// rootPath — exposes the missing segment. Leaf matchers are NOT re-run
|
|
// in this pass, so an ancestor like /home/alice/.env/dist can't
|
|
// false-positive every file beneath it just because ".env" appears in
|
|
// the root context.
|
|
//
|
|
// Pure string-level reasoning over rootPath — no filesystem access, no
|
|
// reliance on cwd — so it composes with the project's fileio sandbox and
|
|
// stays inside the shortcuts-layer constraint against direct fs lookups.
|
|
func isSensitiveCandidate(rootPath string, c htmlPublishCandidate) bool {
|
|
if isSensitiveRelPath(c.RelPath) {
|
|
return true
|
|
}
|
|
for _, ctx := range []string{filepath.Base(rootPath), filepath.Base(filepath.Dir(rootPath))} {
|
|
switch ctx {
|
|
case "", ".", "..", "/":
|
|
continue
|
|
}
|
|
if hasParentAnchoredCredentialPair(filepath.ToSlash(filepath.Join(ctx, c.RelPath))) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|