github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/policy/policy.go (about)

     1  package policy
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"time"
    10  
    11  	"github.com/open-policy-agent/opa/bundle"
    12  	"golang.org/x/xerrors"
    13  	"k8s.io/utils/clock"
    14  
    15  	"github.com/devseccon/trivy/pkg/fanal/types"
    16  	"github.com/devseccon/trivy/pkg/log"
    17  	"github.com/devseccon/trivy/pkg/oci"
    18  )
    19  
    20  const (
    21  	BundleVersion    = 0 // Latest released MAJOR version for defsec
    22  	BundleRepository = "ghcr.io/aquasecurity/trivy-policies"
    23  	policyMediaType  = "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip"
    24  	updateInterval   = 24 * time.Hour
    25  )
    26  
    27  type options struct {
    28  	artifact *oci.Artifact
    29  	clock    clock.Clock
    30  }
    31  
    32  // WithOCIArtifact takes an OCI artifact
    33  func WithOCIArtifact(art *oci.Artifact) Option {
    34  	return func(opts *options) {
    35  		opts.artifact = art
    36  	}
    37  }
    38  
    39  // WithClock takes a clock
    40  func WithClock(c clock.Clock) Option {
    41  	return func(opts *options) {
    42  		opts.clock = c
    43  	}
    44  }
    45  
    46  // Option is a functional option
    47  type Option func(*options)
    48  
    49  // Client implements policy operations
    50  type Client struct {
    51  	*options
    52  	policyDir        string
    53  	policyBundleRepo string
    54  	quiet            bool
    55  }
    56  
    57  // Metadata holds default policy metadata
    58  type Metadata struct {
    59  	Digest       string
    60  	DownloadedAt time.Time
    61  }
    62  
    63  func (m Metadata) String() string {
    64  	return fmt.Sprintf(`Policy Bundle:
    65    Digest: %s
    66    DownloadedAt: %s
    67  `, m.Digest, m.DownloadedAt.UTC())
    68  }
    69  
    70  // NewClient is the factory method for policy client
    71  func NewClient(cacheDir string, quiet bool, policyBundleRepo string, opts ...Option) (*Client, error) {
    72  	o := &options{
    73  		clock: clock.RealClock{},
    74  	}
    75  
    76  	for _, opt := range opts {
    77  		opt(o)
    78  	}
    79  
    80  	if policyBundleRepo == "" {
    81  		policyBundleRepo = fmt.Sprintf("%s:%d", BundleRepository, BundleVersion)
    82  	}
    83  
    84  	return &Client{
    85  		options:          o,
    86  		policyDir:        filepath.Join(cacheDir, "policy"),
    87  		policyBundleRepo: policyBundleRepo,
    88  		quiet:            quiet,
    89  	}, nil
    90  }
    91  
    92  func (c *Client) populateOCIArtifact() error {
    93  	if c.artifact == nil {
    94  		log.Logger.Debugf("Using URL: %s to load policy bundle", c.policyBundleRepo)
    95  		art, err := oci.NewArtifact(c.policyBundleRepo, c.quiet, types.RegistryOptions{})
    96  		if err != nil {
    97  			return xerrors.Errorf("OCI artifact error: %w", err)
    98  		}
    99  		c.artifact = art
   100  	}
   101  	return nil
   102  }
   103  
   104  // DownloadBuiltinPolicies download default policies from GitHub Pages
   105  func (c *Client) DownloadBuiltinPolicies(ctx context.Context) error {
   106  	if err := c.populateOCIArtifact(); err != nil {
   107  		return xerrors.Errorf("OPA bundle error: %w", err)
   108  	}
   109  
   110  	dst := c.contentDir()
   111  	if err := c.artifact.Download(ctx, dst, oci.DownloadOption{MediaType: policyMediaType}); err != nil {
   112  		return xerrors.Errorf("download error: %w", err)
   113  	}
   114  
   115  	digest, err := c.artifact.Digest(ctx)
   116  	if err != nil {
   117  		return xerrors.Errorf("digest error: %w", err)
   118  	}
   119  	log.Logger.Debugf("Digest of the built-in policies: %s", digest)
   120  
   121  	// Update metadata.json with the new digest and the current date
   122  	if err = c.updateMetadata(digest, c.clock.Now()); err != nil {
   123  		return xerrors.Errorf("unable to update the policy metadata: %w", err)
   124  	}
   125  
   126  	return nil
   127  }
   128  
   129  // LoadBuiltinPolicies loads default policies
   130  func (c *Client) LoadBuiltinPolicies() ([]string, error) {
   131  	f, err := os.Open(c.manifestPath())
   132  	if err != nil {
   133  		return nil, xerrors.Errorf("manifest file open error (%s): %w", c.manifestPath(), err)
   134  	}
   135  	defer f.Close()
   136  
   137  	var manifest bundle.Manifest
   138  	if err = json.NewDecoder(f).Decode(&manifest); err != nil {
   139  		return nil, xerrors.Errorf("json decode error (%s): %w", c.manifestPath(), err)
   140  	}
   141  
   142  	// If the "roots" field is not included in the manifest it defaults to [""]
   143  	// which means that ALL data and policy must come from the bundle.
   144  	if manifest.Roots == nil || len(*manifest.Roots) == 0 {
   145  		return []string{c.contentDir()}, nil
   146  	}
   147  
   148  	var policyPaths []string
   149  	for _, root := range *manifest.Roots {
   150  		policyPaths = append(policyPaths, filepath.Join(c.contentDir(), root))
   151  	}
   152  
   153  	return policyPaths, nil
   154  }
   155  
   156  // NeedsUpdate returns if the default policy should be updated
   157  func (c *Client) NeedsUpdate(ctx context.Context) (bool, error) {
   158  	meta, err := c.GetMetadata()
   159  	if err != nil {
   160  		return true, nil
   161  	}
   162  
   163  	// No need to update if it's been within a day since the last update.
   164  	if c.clock.Now().Before(meta.DownloadedAt.Add(updateInterval)) {
   165  		return false, nil
   166  	}
   167  
   168  	if err = c.populateOCIArtifact(); err != nil {
   169  		return false, xerrors.Errorf("OPA bundle error: %w", err)
   170  	}
   171  
   172  	digest, err := c.artifact.Digest(ctx)
   173  	if err != nil {
   174  		return false, xerrors.Errorf("digest error: %w", err)
   175  	}
   176  
   177  	if meta.Digest != digest {
   178  		return true, nil
   179  	}
   180  
   181  	// Update DownloadedAt with the current time.
   182  	// Otherwise, if there are no updates in the remote registry,
   183  	// the digest will be fetched every time even after this.
   184  	if err = c.updateMetadata(meta.Digest, time.Now()); err != nil {
   185  		return false, xerrors.Errorf("unable to update the policy metadata: %w", err)
   186  	}
   187  
   188  	return false, nil
   189  }
   190  
   191  func (c *Client) contentDir() string {
   192  	return filepath.Join(c.policyDir, "content")
   193  }
   194  
   195  func (c *Client) metadataPath() string {
   196  	return filepath.Join(c.policyDir, "metadata.json")
   197  }
   198  
   199  func (c *Client) manifestPath() string {
   200  	return filepath.Join(c.contentDir(), bundle.ManifestExt)
   201  }
   202  
   203  func (c *Client) updateMetadata(digest string, now time.Time) error {
   204  	f, err := os.Create(c.metadataPath())
   205  	if err != nil {
   206  		return xerrors.Errorf("failed to open a policy manifest: %w", err)
   207  	}
   208  	defer f.Close()
   209  
   210  	meta := Metadata{
   211  		Digest:       digest,
   212  		DownloadedAt: now,
   213  	}
   214  
   215  	if err = json.NewEncoder(f).Encode(meta); err != nil {
   216  		return xerrors.Errorf("json encode error: %w", err)
   217  	}
   218  
   219  	return nil
   220  }
   221  
   222  func (c *Client) GetMetadata() (*Metadata, error) {
   223  	f, err := os.Open(c.metadataPath())
   224  	if err != nil {
   225  		log.Logger.Debugf("Failed to open the policy metadata: %s", err)
   226  		return nil, err
   227  	}
   228  	defer f.Close()
   229  
   230  	var meta Metadata
   231  	if err = json.NewDecoder(f).Decode(&meta); err != nil {
   232  		log.Logger.Warnf("Policy metadata decode error: %s", err)
   233  		return nil, err
   234  	}
   235  
   236  	return &meta, nil
   237  }
   238  
   239  func (c *Client) Clear() error {
   240  	log.Logger.Info("Removing policy bundle...")
   241  	if err := os.RemoveAll(c.policyDir); err != nil {
   242  		return xerrors.Errorf("failed to remove policy bundle: %w", err)
   243  	}
   244  	return nil
   245  }