Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,19 @@ $ ossls -audit
ossls: violations found
```

#### Reproducible Builds

For reproducible builds, set the `SOURCE_DATE_EPOCH` environment variable to normalize file timestamps in exported output:

```bash
export SOURCE_DATE_EPOCH=1609459200 # 2021-01-01 00:00:00 UTC
ossls audit --export THIRD_PARTY_NOTICES
```

This ensures that running `ossls audit --export` multiple times with identical inputs produces byte-for-byte identical output, enabling build verification for compliance.

When `SOURCE_DATE_EPOCH` is not set, files are exported with current timestamps.

[circleci-badge]: https://circleci.com/gh/stackrox/ossls.svg?&style=shield&circle-token=5ac8a87fbadae84c41f8c1fc868ad5d8ba85c90e
[circleci-link]: https://circleci.com/gh/stackrox/ossls/tree/master
[github-release-link]: https://github.com/stackrox/ossls/releases/latest
51 changes: 41 additions & 10 deletions cmd/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"

"github.com/fatih/color"
"github.com/pkg/errors"
Expand Down Expand Up @@ -117,11 +119,13 @@ func AuditCommand() *cobra.Command {
return errors.Wrap(err, "resolving dependencies")
}

exportTime := getExportTimestamp()

var failures bool
for _, dependency := range dependencies {
var err error
if exportFlag != "" {
err = export(dependency, exportFlag)
err = export(dependency, exportFlag, exportTime)
}

if err != nil {
Expand Down Expand Up @@ -173,6 +177,7 @@ func joinDeps(patterns config.PatternConfig, sets ...map[string]resolver.Depende
if err != nil {
return nil, errors.Wrapf(err, "finding license files in directory %s", dependency.SourceDir)
}
sort.Strings(files)
dependency.Alias = flattenName(name)
dependency.Files = files
dependencies = append(dependencies, dependency)
Expand All @@ -185,16 +190,26 @@ func joinDeps(patterns config.PatternConfig, sets ...map[string]resolver.Depende
return dependencies, nil
}

func export(dependency resolver.Dependency, destination string) error {
if err := os.MkdirAll(filepath.Join(destination, dependency.Alias), 0755); err != nil {
// getExportTimestamp returns the timestamp to use for exported files.
// If SOURCE_DATE_EPOCH is set and valid, uses that timestamp.
// Otherwise, returns the current time.
func getExportTimestamp() time.Time {
if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
if sec, err := strconv.ParseInt(epoch, 10, 64); err == nil {
return time.Unix(sec, 0)
}
}
return time.Now()
}

func export(dependency resolver.Dependency, destination string, timestamp time.Time) error {
destDir := filepath.Join(destination, dependency.Alias)
if err := os.MkdirAll(destDir, 0755); err != nil {
return err
}

for _, file := range dependency.Files {
if err := exportDependencyFile(
file,
filepath.Join(destination, dependency.Alias),
); err != nil {
if err := exportDependencyFile(file, destDir, timestamp); err != nil {
return err
}
}
Expand All @@ -210,6 +225,13 @@ func exportManifest(destination string, dependencies []resolver.Dependency) erro
}
defer file.Close()

sort.Slice(dependencies, func(i, j int) bool {
if !strings.EqualFold(dependencies[i].Name, dependencies[j].Name) {
return strings.ToLower(dependencies[i].Name) < strings.ToLower(dependencies[j].Name)
}
return dependencies[i].Version < dependencies[j].Version
})

fmt.Fprintln(file, "Name,Version,Directory")
for _, dep := range dependencies {
fmt.Fprintf(file, "%s,%s,./%s\n", dep.Name, dep.Version, dep.Alias)
Expand Down Expand Up @@ -244,14 +266,23 @@ func flattenName(name string) string {
return strings.Replace(name, "/", "-", -1)
}

func exportDependencyFile(src, dstDir string) error {
func exportDependencyFile(src, dstDir string, timestamp time.Time) error {
dstFile := filepath.Base(src)
var dstPath string
if strings.ToLower(dstFile) == "package.json" {
// Do not directly copy the package json file to avoid false positives
// from image scanners for developer dependencies -- only export a subset of fields.
return copyPackageJsonContents(src, filepath.Join(dstDir, "license-info.json"))
dstPath = filepath.Join(dstDir, "license-info.json")
if err := copyPackageJsonContents(src, dstPath); err != nil {
return err
}
} else {
dstPath = filepath.Join(dstDir, dstFile)
if err := copyFileContents(src, dstPath); err != nil {
return err
}
}
return copyFileContents(src, filepath.Join(dstDir, dstFile))
return os.Chtimes(dstPath, timestamp, timestamp)
}

func copyJsonFieldIfExists(fieldName string, in, out map[string]interface{}) {
Expand Down
Loading