Compare commits

..

368 Commits

Author SHA1 Message Date
Andy 0085d3e042
update readme
continuous-integration/drone/push Build is passing Details
5 years ago
Andy 0e5c4b6445
add Drone CI
5 years ago
Jimmy Zelinskie d0bd4c7ab8
Merge pull request #773 from flumm/disco
5 years ago
Dominik Csapak e08fe792ef add disco to namespace mappings for ubuntu
5 years ago
Jimmy Zelinskie 5fef44dd04
Merge pull request #671 from ericysim/amazon
5 years ago
Eric Sim 32cd4f1ec3 Add Amazon Linux to drivers and data sources doc
5 years ago
Eric Sim 6617f560cc database: Rename affected type to feature type (for Amazon Linux updater)
5 years ago
Eric Sim adde75975f Fix style issues
5 years ago
Eric Sim 684ae2be1d Refactoring (minor)
5 years ago
Eric Sim 8e98ee878a Add 2xx checks for mirror.list and repomd.xml
5 years ago
Eric Sim 803cf4a29e gofmt
5 years ago
Eric Sim 8fb9097dbd Add updaters for Amazon Linux 2018.03 and Amazon Linux 2
5 years ago
Jimmy Zelinskie d79827690c
Merge pull request #766 from Allda/lock_timeout
5 years ago
Ales Raszka 4e49aaf346 Fix: lock updater - return correct bool value
5 years ago
Jimmy Zelinskie 3316e7e1ef
Merge pull request #742 from bluelabsio/path-templating
5 years ago
Jimmy Zelinskie e8bd0c4f78
Merge pull request #739 from joelee2012/master
5 years ago
Jimmy Zelinskie 4af5afe305
Merge pull request #749 from cnorthwood/tarutil-glob
5 years ago
Chris Northwood a3a37072b5 tarutil: convert all filename specs to regexps
5 years ago
Chris Northwood afd7fe2554 tarutil: allow file names to be specified by regexp
5 years ago
Sida Chen 1234a8d2f0
Merge pull request #741 from KeyboardNerd/parallel_download
5 years ago
Sida Chen 098cb2ef2c
Merge pull request #738 from Allda/potentialNamespaceAncestry
5 years ago
Kate Hill 710c65530f helm: allow for ingress path configuration in values.yml
5 years ago
Sida Chen 88f506918b v3: Analyze layer content in parallel
5 years ago
Ales Raszka f2ce8325b9 Convert: return nil when detector is empty
5 years ago
Li Joe 3f13184ad6 Add claircli tool to interact with clair
5 years ago
Ales Raszka f326b6f664 Use PotentialNamespace in ancestry
5 years ago
Sida Chen 2c7838eac7
Merge pull request #721 from KeyboardNerd/cache
5 years ago
Sida Chen 2d1ac2c4d5 update
5 years ago
Sida Chen 0731df972c pgsql: Remove unused test code
5 years ago
Sida Chen dfa07f6d86 pgsql: Move notification to its module
5 years ago
Sida Chen 921acb26fe pgsql: Split vulnerability.go to files in vulnerability module
5 years ago
Sida Chen 7cc83ccbc5 pgsql: Split ancestry.go to files in ancestry module
5 years ago
Sida Chen 497b79a293 pgsql: Add test for migrations
5 years ago
Sida Chen ea418cffd4 pgsql: Split layer.go to files in layer module
5 years ago
Sida Chen 176c69e59d pgsql: Move namespace to its module
5 years ago
Sida Chen 98e81ff5f1 pgsql: Move keyvalue to keyvalue module
5 years ago
Sida Chen ba50d7c626 pgsql: Move lock to lock module
5 years ago
Sida Chen 0b32b36cf7 pgsql: Move detector to pgsql/detector module
5 years ago
Sida Chen c50a2339b7 pgsql: Split feature.go to table based files in feature module
5 years ago
Sida Chen 43f3ea87d8 pgsql: Move batch queries to corresponding modules
5 years ago
Sida Chen a33050637b pgsql: Move extra logic in pgsql.go to util folder
5 years ago
Sida Chen 8bebea3643 pgsql: Split testutil.go into multiple files
5 years ago
Sida Chen 3fafb73c4f database: Split models.go into different files each contains one model
5 years ago
Jimmy Zelinskie a2d6508730
Merge pull request #735 from jzelinskie/fix-sweet32
5 years ago
Jimmy Zelinskie c4a32543e8 pkg/grpcutil: use cockroachdb cipher suite
5 years ago
Sida Chen a689f1f1dc
Merge pull request #722 from Allda/feature_ns
5 years ago
Ales Raszka d77dc0f0ae Use struct as a map key instead of string
5 years ago
Ales Raszka a8a91379d9 Add test for potential namespace
5 years ago
Ales Raszka 60b0bd27fa Add namespace_id as constraints to layer_feature
5 years ago
Ales Raszka 60ef726677 Move PotentialNamespace to LayerFeature
5 years ago
Ales Raszka 44c4a6f3ce Store PotentialNamespace in database
5 years ago
Ales Raszka 34c2d96b36 featurefmt: Extract PotentialNamespace
5 years ago
Sida Chen b3fe95e152
Merge pull request #724 from KeyboardNerd/ref
5 years ago
Sida Chen 1b9ed99646 database: Move db logic to dbutil
5 years ago
Sida Chen 1f0bc1ea5f
Merge pull request #728 from KeyboardNerd/fix
5 years ago
Sida Chen 58f014c297
Merge pull request #727 from KeyboardNerd/master
5 years ago
Sida Chen b03f1bc3a6 pgsql: Fix failed tests
5 years ago
Sida Chen ed9c6baf4f pgsql: Fix pgsql test
5 years ago
Sida Chen f6759b2a15
Merge pull request #725 from KeyboardNerd/license_test
5 years ago
Sida Chen 046b0e49d1 Add missing licenses
5 years ago
Sida Chen 07a97e30c6 Add license header check test
5 years ago
Jimmy Zelinskie 4fa03d1c78
Merge pull request #723 from jzelinskie/lock-tx
5 years ago
Jimmy Zelinskie 961c7d4680 database: add test for lock expiration
5 years ago
Jimmy Zelinskie a4e7873d14 database: make locks SOI & add Extend method
5 years ago
Sida Chen fb209d32a0
Merge pull request #720 from KeyboardNerd/update_ns
5 years ago
Sida Chen c6497dda0a clair: Fix namespace update logic
5 years ago
Sida Chen f7e54c1a28
Merge pull request #695 from saromanov/fix-unchecked-error
5 years ago
Sida Chen 76081864c9
Merge pull request #712 from KeyboardNerd/builder
5 years ago
Sida Chen 5bf8365f7b pgsql: Prevent inserting invalid entry to database
5 years ago
Sida Chen 465687fa94 clair: Add more logging on ancestry cache hit
5 years ago
Sida Chen 8aae73f1c8 pgsql: Remove unnecessary logs
5 years ago
Sida Chen 5b2376498b clair: Use builder pattern for constructing ancestry
5 years ago
Sida Chen 891ce1697d imagefmt: Move layer blob download logic to blob.go
5 years ago
Sida Chen dd239762f6 v3: Move services to top of the file
5 years ago
Sida Chen 73bc2bc36b
Merge pull request #672 from KeyboardNerd/source_package/feature_type
5 years ago
Sida Chen 5a94499fdb update according to comments
5 years ago
Sida Chen 32b11e54eb api/v3: Add feature type to API feature
5 years ago
Sida Chen 870e812376 Travis: Drop support for postgres 9.4
5 years ago
Sida Chen 23ccd9b53b worker: Fix tests for feature_type
5 years ago
Sida Chen 79af05e67d pgsql: Fix postgres queries for feature_type
5 years ago
Sida Chen 5fa1ac89b9 database: Add StorageError type
5 years ago
Sida Chen 073c685c5b pgsql: Add proper tests for database migration
5 years ago
Sida Chen f61675355e database: Update feature model
5 years ago
Sida Chen 0e0d8b38bb featurefmt: Extract source packages and binary packages
5 years ago
Sida Chen 7dd989c0f2 database: Rename affected Type to feature type
5 years ago
Sida Chen 00eed77b45 database: Add feature_type database model
5 years ago
Sida Chen c6c8fce39a pgsql: Add feature_type to initial schema
5 years ago
Jimmy Zelinskie cafe0976a4
Merge pull request #685 from jzelinskie/updater-cleanup
5 years ago
Jimmy Zelinskie dd91597f19 database: remove FindLock from mock
5 years ago
Jimmy Zelinskie f64bd117b2 updater,pkg/timeutil: minor cleanups
5 years ago
Jimmy Zelinskie aa8682947e
Merge pull request #701 from dustinspecker/patch-1
5 years ago
Jimmy Zelinskie 11b26b3857
Merge pull request #700 from traum-ferienwohnungen/master
5 years ago
Dustin Specker 49b5621d73
docs: fix typo in running-clair
5 years ago
Jelto Wodstrcil 4505fcea32 make nodePort configurable with helm
5 years ago
Jimmy Zelinskie cc8d1152c4
Merge pull request #680 from Allda/slices
5 years ago
Sergey a57d806717
pgsql: fix unchecked error
5 years ago
Ales Raszka 015a79fd5a Layer: replace arrays with slices
5 years ago
Ales Raszka 90f5592095 Feature: replace arrays with slices
5 years ago
Jimmy Zelinskie 97b4b1ac33
Merge pull request #687 from jzelinskie/suse-config
5 years ago
Jimmy Zelinskie 162e8cdafc config: enable suse updater
5 years ago
Jimmy Zelinskie bafe45db2d
Merge pull request #686 from jzelinskie/fix-presentations
5 years ago
Jimmy Zelinskie 3e6896c6a4 documentation: fix links to presentations
5 years ago
Jimmy Zelinskie 165c397f16 glide: add errgroup and regenerate vendor
5 years ago
Jimmy Zelinskie 7084a226ae updater: extract deduplicate function
5 years ago
Jimmy Zelinskie 25078ac838 ext: add CleanAll() utility functions
5 years ago
Jimmy Zelinskie e16d17dda9 updater: remove original RunUpdate()
5 years ago
Jimmy Zelinskie 0d41968acd updater: reimplement fetch() with errgroup
5 years ago
Jimmy Zelinskie 6c5be7e1c6 updater: refactor to use errgroup
5 years ago
Jimmy Zelinskie 399deab100 database: remove FindLock()
5 years ago
Jimmy Zelinskie effe1552fb
Merge pull request #679 from kubeshield/master
5 years ago
Jimmy Zelinskie 45ecf18815 pkg/timeutil: init
5 years ago
Jimmy Zelinskie b08ad9b8e6
Merge pull request #506 from openSUSE/reintroduce-suse-opensuse
5 years ago
Flavio Castelli 1105102b84
Update documentation: talk about SUSE support
5 years ago
Flavio Castelli 5a4d4913c1
Reintroduce image scanning for openSUSE and SLE
5 years ago
Jimmy Zelinskie 5cd6a8cc92
Merge pull request #681 from Allda/rhel_severity
5 years ago
Tamal Saha 0ed4126240 Fix cert and key file mix up
5 years ago
Ales Raszka bd7102d963 Vulnsrc rhel: handle "none" CVE impact
5 years ago
Jimmy Zelinskie 3947073b9e
Merge pull request #667 from travelaudience/helm-tolerations
6 years ago
Jeff Knurek 81430ffbb2 HELM: also add option for nodeSelector
6 years ago
Jeff Knurek 6a94d8ccd2 HELM: add option for tolerations
6 years ago
Jimmy Zelinskie 300bb52696 database: add FindLock dbutil
6 years ago
Jimmy Zelinskie 4fbeb9ced5 database: add (Acquire|Release)Lock dbutils
6 years ago
Jimmy Zelinskie 504f0f3af3
Merge pull request #656 from glb/elsa_CVEID
6 years ago
Geoff Baskwill 3503ddb96f vulnsrc_oracle: one vulnerability per CVE
6 years ago
Jimmy Zelinskie 93e7a4cfa8
Merge pull request #650 from Katee/add-ubuntu-cosmic
6 years ago
Jimmy Zelinskie 4c08c8f959
Merge pull request #653 from brosander/helm-dep
6 years ago
Bryan Rosander 00db964497
Pinning helm postgres dep to the working 1.0.0
6 years ago
Kate Murphy 6c682da3e1
database: add mapping for Ubuntu Cosmic (18.10)
6 years ago
Jimmy Zelinskie c123c95590
Merge pull request #648 from HaraldNordgren/go_versions
6 years ago
Harald Nordgren be24096183 Bump Go versions and use '.x' to always get latest patch versions
6 years ago
Sida Chen 05cbf328aa
Merge pull request #647 from KeyboardNerd/spkg/cvrf
6 years ago
Sida Chen 4106322107 vendor: Update gopkg.in/yaml.v2 package
6 years ago
Sida Chen 72674ca871 vulnsrc: Refactor vulnerability sources to use utility functions
6 years ago
Sida Chen a3f7387ff1 database: Add FindKeyValue function wrapper
6 years ago
Sida Chen c3904c9696 pkg: Add fsutil to contian file system utility functions
6 years ago
Sida Chen 1ee1b95afc
Merge pull request #644 from KeyboardNerd/bug/git
6 years ago
Jimmy Zelinskie 0c2e5e73c2
Merge pull request #645 from Katee/include-cvssv3
6 years ago
Kate Murphy 081ae34af1
ext: remove duplicate vectorValuesToLetters definition
6 years ago
Kate Murphy 4f0da12b12
ext: pass through CVSSv3 impact and exploitability score
6 years ago
Jimmy Zelinskie 8efc3e4038 ext: remove unneeded use of init()
6 years ago
Jimmy Zelinskie 699d1143e5 ext: fixup incorrect copyright year
6 years ago
Sida Chen 335cb65917
Merge pull request #646 from KeyboardNerd/spkg/model
6 years ago
Sida Chen 2236b0a5c9 updater: Add vulnsrc affected feature type
6 years ago
Sida Chen 00fadfc3e3 database: Add affected feature type
6 years ago
Sida Chen 11b67e612c gitutil: Fix git pull on non-git repository directory
6 years ago
Kate Murphy b81e4454fb
ext: Parse CVSSv3 data from JSON NVD feed
6 years ago
Kate Murphy 14277a8f5d
ext: Add JSON NVD parsing tests
6 years ago
Kate Murphy aab46f5658
ext: Parse NVD JSON feed instead of XML
6 years ago
Sida Chen 17539bda60
Merge pull request #640 from KeyboardNerd/sourcePackage
6 years ago
Sida Chen f759dd54c0 database: Replace Parent Feature with source metadata
6 years ago
Jimmy Zelinskie 2ac088dd0f
Merge pull request #639 from Katee/update-sha1-to-sha256
6 years ago
Sida Chen fe614f2b01
Merge pull request #638 from KeyboardNerd/featureTree
6 years ago
Kate Murphy 8d5a0131c4
ext: Use SHA256 instead of SHA1 for fingerprinting
6 years ago
Sida Chen 2cc61f9fc0 ext/featurefmt/apk: Extract origin package information from database
6 years ago
Sida Chen a057e4a943 ext/featurefmt/rpm: Extract source package from rpm database
6 years ago
Sida Chen 4ac046642f ext/featurefmt/dpkg: Extract source package metadata
6 years ago
Sida Chen 1c40e7d016 ext/featurefmt: Refactor featurefmt testing code
6 years ago
Sida Chen 3fe894c5ad database: Add parent feature pointer to Feature struct
6 years ago
Jimmy Zelinskie ddaf19b3a6
Merge pull request #633 from coreos/roadmap-1
6 years ago
Sida Chen 3c72fa29a6
Merge pull request #620 from KeyboardNerd/feature/detector
6 years ago
Jimmy Zelinskie 74efdf6b51
*: update roadmap
6 years ago
Sida Chen 69c0c84348 api: Rename detector type to DType
6 years ago
Sida Chen a3e9b5b55d database: rename utility functions with commit/rollback
6 years ago
Sida Chen e657d26313 database: move dbutil and testutil to database from pkg
6 years ago
Sida Chen 0c1b80b2ed pgsql: Implement database queries for detector relationship
6 years ago
Sida Chen 028324014b clair: Implement worker detector support
6 years ago
Sida Chen 48427e9b88 api: Add detectors for RPC
6 years ago
Sida Chen 9c49d9dc55 pgsql: Move queries to corresponding files
6 years ago
Sida Chen 53bf19aecf ext: Lister and Detector returns detector info with detected content
6 years ago
Sida Chen 34d0e516e0 vendor: Add golang-set dependency
6 years ago
Sida Chen dca2d4e597 pgsql: Add detector to database schema
6 years ago
Sida Chen db2db8bbe8 database: Update database model and interface for detectors
6 years ago
Sida Chen 9f5d1ea4e1 v3: associate feature and namespace with detector
6 years ago
Jimmy Zelinskie 8cf7ad454c
Merge pull request #627 from haydenhughes/master
6 years ago
Jimmy Zelinskie 5d1c30218e
Merge pull request #624 from jzelinskie/probot
6 years ago
Jimmy Zelinskie 9b1f205833 .github: add stale and issue template enforcement
6 years ago
Jimmy Zelinskie 0ca9431235
Merge pull request #621 from jzelinskie/gitutil
6 years ago
Hayden Hughes d3facfd7cd
Add build-base to docker image
6 years ago
Sida Chen 0609ed964b config: removed worker config
6 years ago
Sida Chen 53433090a3 pgsql: update the query format
6 years ago
Jimmy Zelinskie 44ae4bc959
Merge pull request #610 from MackJM/wip/master_nvd_httputil
6 years ago
Jimmy Zelinskie c2d887f9e9 pkg/gitutil: init
6 years ago
Jimmy Zelinskie d0a3fe9206
Merge pull request #499 from yebinama/rhel_CVEID
6 years ago
Grégoire Unbekandt c4ffa0c370 vulnsrc_rhel: cve impact
6 years ago
Grégoire Unbekandt a90db713a2 vulnsrc_rhel: add test
6 years ago
Grégoire Unbekandt 8b3338ef56 vulnsrc_rhel: minor changes
6 years ago
Grégoire Unbekandt 4e4e98f328 vulnsrc_rhel: minor changes
6 years ago
Grégoire Unbekandt ac86a36740 vulnsrc_rhel: rhsa_ID by default
6 years ago
Grégoire Unbekandt 4ab98cfe54 vulnsrc_rhel: one vulnerability by CVE
6 years ago
Sida Chen f98ff58afd
Merge pull request #619 from KeyboardNerd/sidac/rm_layer
6 years ago
Sida Chen e160616723 database: Use LayerWithContent as Layer
6 years ago
Jean Michel MacKay 30848d9eb7 Fixed extra newline
6 years ago
Jean Michel MacKay 56b4f23ae2 Move downloadFeed out to a seperate function
6 years ago
Jean Michel MacKay f34f94320a Embed nvd's downloading and storing of meta data into a function to help with resource management
6 years ago
Jean Michel MacKay 3959f416fa Fix up error and changing close to defer close
6 years ago
Jean Michel MacKay 49cbdd7a7c Using httputil for NVD
6 years ago
Jimmy Zelinskie 089a4e0f0a
Merge pull request #617 from jzelinskie/grpc-refactor
6 years ago
Jimmy Zelinskie 1ec2759550 pkg/grpcutil: init
6 years ago
Sida Chen ff9303905b database: changed Notification interface name
6 years ago
Sida Chen 6c69377343
Merge pull request #614 from KeyboardNerd/sidac/simplify
6 years ago
Jimmy Zelinskie dc6be5d1b0 api: remove handleShutdown func
6 years ago
Sida Chen 5d725e67b0 Replace Ancestry with AncestryWithContent struct in database models
6 years ago
Jimmy Zelinskie e5c2e378a2
Merge pull request #613 from jzelinskie/pkg-pagination
6 years ago
Jimmy Zelinskie 0565938956 pkg/pagination: add token type
6 years ago
Jimmy Zelinskie d193b46449 pkg/pagination: init
6 years ago
Jimmy Zelinskie b20482e0ae cmd/clair: document constants
6 years ago
Jimmy Zelinskie fffb67f137
Merge pull request #611 from jzelinskie/drop-graceful
6 years ago
Jimmy Zelinskie 55ecf1e58a vendor: regenerate after removing graceful
6 years ago
Jimmy Zelinskie 30644fcc01 api: remove dependency on graceful
6 years ago
Sida Chen 2bbbad393b
Merge pull request #605 from KeyboardNerd/sidchen/feature
6 years ago
Sida Chen 2827b9342b Update Database and Worker implementation for layer-wise feature
6 years ago
Jimmy Zelinskie 06b257cc97
Merge pull request #606 from MackJM/wip/master_httputil
6 years ago
Jimmy Zelinskie 4fd86fd518
Merge pull request #607 from jzelinskie/gofmt
6 years ago
Jimmy Zelinskie ce15f73501 *: gofmt -s
6 years ago
Jimmy Zelinskie 52ecf35ca6 travis: fail if not gofmt -s
6 years ago
Jean Michel MacKay 9df4f5bd70 Adding httputil and version packages
6 years ago
Sida Chen 4b64151330 Update gRPC server implementation
6 years ago
Sida Chen 6a44052e31 Update Clair V3 API to provide layer-wise feature
6 years ago
Jimmy Zelinskie 9f2cc4e533
Merge pull request #604 from jzelinskie/nvd-urls
6 years ago
Jimmy Zelinskie ce6b00887b vulnmdsrc: update NVD URLs
6 years ago
Sida Chen dfc3023372
Merge pull request #601 from KeyboardNerd/sidchen/status
6 years ago
Sida Chen d28f3214ce Add Status endpoint with Clair configuration
6 years ago
Jimmy Zelinskie 7f9c0b1b07
Merge pull request #594 from reasonerjt/fix-alpine-url
6 years ago
Daniel Jiang 9e4a347ecd Quickfix to the URL for fetching alpine's vuln data.
6 years ago
Jimmy Zelinskie ddeb64339d
Merge pull request #578 from naibaf0/fix/helmtemplate/configmap/postgresql
6 years ago
Fabian Hinz 690d26edba Helm: change postgresql connection string format in configmap template
6 years ago
Jimmy Zelinskie 1d690bbacf
Merge pull request #586 from robertomlsoares/update-helm-chart
6 years ago
Roberto Soares bc6f37f1ae Helm Chart: Use Secret for config file. Fix some minor issues
6 years ago
Jimmy Zelinskie c26154ab74
Merge pull request #582 from brosander/helm-alpine-postgres
6 years ago
Bryan Rosander f3e156a46e
Using alpine postgres image
6 years ago
Jimmy Zelinskie b1cd092319
Merge pull request #571 from ErikThoreson/nvdupdates
6 years ago
Jimmy Zelinskie f32f438a98
Merge branch 'master' into nvdupdates
6 years ago
Jimmy Zelinskie 3babbafb2f
Merge pull request #574 from hongli-my/fix-nvd-path
6 years ago
honglichang(常红立) 0d5f300c5b fix nvd path
6 years ago
Jimmy Zelinskie 9a9b1f7a13
Merge pull request #572 from arno01/multi-stage
6 years ago
Andy 921ba54152
use multi-stage build
6 years ago
ErikThoreson df1dd5c149 adding publisher datetime and updating nvd feed download
6 years ago
Jimmy Zelinskie 158bb31b77
Merge pull request #540 from jzelinskie/document-proto
6 years ago
Jimmy Zelinskie 027f239e1f
Merge pull request #569 from jzelinskie/ubuntu-git
6 years ago
Jimmy Zelinskie 5caa821c80 *: remove bzr dependency
6 years ago
Jimmy Zelinskie 456af5f48c vulnsrc/ubuntu: use new git-based ubuntu tracker
6 years ago
Jimmy Zelinskie c031f8ea0c vulnsrc/alpine: s/pull/clone
6 years ago
Jimmy Zelinskie 4c2be5285e vulnsrc/alpine: avoid shadowing vars
6 years ago
Jimmy Zelinskie e907e4d263
Merge pull request #553 from qeqar/master
6 years ago
Mark Eisenblaetter 07a08a4f53 mapping: add ubuntu mapping
6 years ago
Jimmy Zelinskie 34c1382d9e
Merge pull request #551 from usr42/upgrade_to_1.10-alpine
6 years ago
usr42 db5dbbe4e9 Upgrade to golang:1.10-alpine
6 years ago
Jimmy Zelinskie 7492aa31ba travis: fail unformatted protos
6 years ago
Jimmy Zelinskie f550dd16a0 api/v3: remove dependency on google empty message
6 years ago
Jimmy Zelinskie d7a751e0d4 api/v3: prototool format
6 years ago
Jimmy Zelinskie 6b9f668ea0 api/v3/clairpb: document and regenerate protos
6 years ago
Jimmy Zelinskie a5b3e747a0
Merge pull request #538 from jzelinskie/dockerize-protogen
6 years ago
Jimmy Zelinskie 8c71427375
Merge pull request #537 from tomer-1/patch-1
6 years ago
Jimmy Zelinskie ec5014f8a1 api/v3/clairpb: regen protobufs
6 years ago
Jimmy Zelinskie 389b6e9927 api/v3/clairpb: generate protobufs in docker
6 years ago
Tomer H e649f8f149
Update values.yaml
6 years ago
Jimmy Zelinskie e73051fc0a
Merge pull request #532 from KeyboardNerd/readme_typo
6 years ago
Sida Chen 4db72b8c26 README: fixed issues address
6 years ago
Brad Ison 8a2ed864b9
Merge pull request #508 from joerayme/bug/436
6 years ago
Jimmy Zelinskie 3d2c12cd56
Merge pull request #528 from KeyboardNerd/helm_typo
6 years ago
Sida Chen 7a06a7a2b4 Helm: Fixed a typo in maintainers field.
6 years ago
Jimmy Zelinskie 5e7b450be9
Merge pull request #522 from vdboor/master
6 years ago
Diederik van der Boor e454314beb Cleanup and improve chart values
6 years ago
Jimmy Zelinskie 01eb48bc84
Merge pull request #521 from yebinama/paclair
6 years ago
Jimmy Zelinskie 69cfe9213f
Merge pull request #518 from traum-ferienwohnungen/master
6 years ago
Diederik van der Boor 64c2853e75 Fix helm chart to generate config.postgresURI
6 years ago
Grégoire Unbekandt c1a58bf922 Documentation: add new 3rd party tool
6 years ago
Jelto Wodstrcil e9dba0fa8f fix whitespace remove in postgresql configmap helm chart
6 years ago
Jimmy Zelinskie ce0699c59d
Merge pull request #513 from leandrocr/patch-1
6 years ago
Jimmy Zelinskie 12b47b0854
Merge pull request #517 from KeyboardNerd/master
6 years ago
Sida Chen a75b8ac7ff api,database: updated version_format documentation.
6 years ago
Leandro Repolho 45dfabbfea
Improve documentation on helm installation
6 years ago
Joe Ray 947a8aa00c featurens: Ensure RHEL is correctly identified
6 years ago
Jimmy Zelinskie 52a42b8503
Merge pull request #505 from ericchiang/coc
6 years ago
Eric Chiang e43ec26965 update CoC
6 years ago
Jimmy Zelinskie 30bd568d83
Merge pull request #484 from odg0318/master
7 years ago
odg0318 821fb240bc Usage of Running clair via docker is wrongly written.
7 years ago
Jimmy Zelinskie e2497ec214
Merge pull request #498 from bkochendorfer/contributing-link
7 years ago
Brett Kochendorfer 6343119215
Update CONTRIBUTING.MD link
7 years ago
Jimmy Zelinskie db6379bc9e
Merge pull request #482 from yfoelling/patch-1
7 years ago
Jimmy Zelinskie 838dcb0872
Merge pull request #487 from ajgreenb/db-connection-backoff
7 years ago
Aaron Greenberg 46bf4c470a Fix logging of error when connecting to database
7 years ago
Aaron Greenberg 0df9e7075c Make max DB connection attempts a constant
7 years ago
Jimmy Zelinskie 1425086670
Merge pull request #488 from caulagi/patch-1
7 years ago
Pradip Caulagi f97f90835f
Fix typo with path name for values.yaml in helm chart
7 years ago
Aaron Greenberg 66b116ecb2 Use a linear backoff when connecting to DB
7 years ago
Jimmy Zelinskie 39f00cd756
Merge pull request #485 from yebinama/proxy
7 years ago
Grégoire Unbekandt 5c5857548d driver: Add proxy support
7 years ago
Yann Fölling 3a3a0c07f7
Added my new python project Yair
7 years ago
Jimmy Zelinskie e00e57cc2f
Merge pull request #481 from coreos/stable-release-issue-template
7 years ago
Jimmy Zelinskie c7a67edf5d
github: add issue template stable release notice
7 years ago
Jimmy Zelinskie 1e9008a333 Merge pull request #479 from yebinama/nvd_vectors
7 years ago
Grégoire Unbekandt e953a259b0 nvd: fix the name of a field
7 years ago
Jimmy Zelinskie db91b91f38 Merge pull request #477 from bseb/master
7 years ago
Jimmy Zelinskie fbd8407c72 Merge pull request #469 from zamarrowski/master
7 years ago
Ben Sebastian 36917c84ec Use apk to install dumb-init instead of curl, modify entrypoint
7 years ago
Jimmy Zelinskie 00f9c06081 Merge pull request #475 from dctrud/clair-singularity
7 years ago
Ben Sebastian d60993c025 Add dumb-init to Entrypoint
7 years ago
Ben Sebastian 57e8fdeb10 Add dumb-init to Entrypoint
7 years ago
David Trudgian 826aaf9e88 Add clair-singularity to integrations.md
7 years ago
Sergio Zamarro 89a5a80cf1 Fix config.yaml.sample
7 years ago
Jimmy Zelinskie 148b3e075d Merge pull request #467 from grebois/master
7 years ago
Marcelo Grebois 3617b7a126 Adding back postgresURI and correcting comments
7 years ago
Marcelo Grebois 79510232b8 Adding postgres to the chart so it can be used without dependecies.
7 years ago
Jimmy Zelinskie 38715290a4 Merge pull request #465 from jzelinskie/github
7 years ago
Jimmy Zelinskie f6cac4733a github: add issue template
7 years ago
Jimmy Zelinskie 24ca12bdec github: move CONTRIBUTING to github dir
7 years ago
Jimmy Zelinskie 4d83c9c88b Merge pull request #463 from brunomcustodio/fix-ingress
7 years ago
Bruno Miguel Custodio 13be17a690
contrib/helm/clair: fix the ingress template
7 years ago
Jimmy Zelinskie e4a58305f4 Merge pull request #459 from arthurlm44/patch-1
7 years ago
Arthur Le Maitre 6bd3458ae7 backtick helm init for prettier rendering
7 years ago
Arthur Le Maitre ccf263789c update documentation for kubernetes / helm install
7 years ago
Jimmy Zelinskie 6ad4aeab76 Merge pull request #458 from jzelinskie/linux-vulns
7 years ago
Jimmy Zelinskie 1f288ab3c3 Merge pull request #450 from jzelinskie/move-token
7 years ago
Arthur Le Maitre 99f1870115 update first instructions for k8s with helm
7 years ago
Jimmy Zelinskie 9ee2ff4877 docs: add troubleshooting about kernel packages
7 years ago
Jimmy Zelinskie cdd214b889 Merge pull request #454 from InTheCloudDan/helm-tls-option
7 years ago
Jimmy Zelinskie cbd8d0550b Merge pull request #455 from zmarouf/master
7 years ago
zmarouf bd68578b8b
style: Fix typo in headline
7 years ago
Daniel O'Brien 009e2457f2 adding insecure TLS to Helm chart
7 years ago
Jimmy Zelinskie 44b9701c94 Merge pull request #449 from jzelinskie/helm
7 years ago
Jimmy Zelinskie 4491bedf2e database/pgsql: move token lib
7 years ago
Jimmy Zelinskie 76b9f8ea05 contrib: replace old k8s manifests with helm
7 years ago
Jimmy Zelinskie 355f1e6d98 Merge pull request #447 from KeyboardNerd/ancestry_
7 years ago
Sida Chen dc8704a961 Merge branch 'master' into ancestry_
7 years ago
Sida Chen 0151dbaef8 API: change api port to api addr, rename RunV2 to Run.
7 years ago
Jimmy Zelinskie d5b987440a Merge pull request #448 from jzelinskie/woops
7 years ago
Jimmy Zelinskie 6c3b398607 README: fix IRC copypasta
7 years ago
Jimmy Zelinskie 9fd691ac9e Merge pull request #444 from jzelinskie/docs-refresh
7 years ago
Jimmy Zelinskie 033cae7d35 *: regenerate bill of materials
7 years ago
Jimmy Zelinskie ac1cdd03c9 contrib: move grafana and compose here
7 years ago
Jimmy Zelinskie 1f5bc26320 *: rename example config
7 years ago
Jimmy Zelinskie 3f91bd2a9b docs: turn README into full articles
7 years ago
Jimmy Zelinskie 6663bcef27 Merge pull request #432 from KeyboardNerd/ancestry_
7 years ago
Sida Chen 58022d97e3 api: renamed V2 API to V3 API for consistency.
7 years ago
Sida Chen 57a4f97780 pgSQL: fixed invalidating vulnerability cache query.
7 years ago
Sida Chen a5c6400065 database: postgres implementation with tests.
7 years ago
Sida Chen fb32dcfa58 Clair Logic, Extensions: updated mock tests, extensions, basic logic
7 years ago
Sida Chen 57b146d0d8 Datastore: updated for Clair V3, decoupled interfaces and models
7 years ago
Sida Chen a378cb070c API: drop v1 api, changed v2 api for Clair v3.
7 years ago
Jimmy Zelinskie 2f08cf52f6 Merge pull request #442 from arminc/add-integration-clari-scanner
7 years ago
Armin 865b92da04 Fix the confusing text
7 years ago
Armin ebc6bff36e Adding clair-scanner as an integration tool
7 years ago
Jimmy Zelinskie 71cce52d3f Merge pull request #433 from mssola/portus-integration
7 years ago
Miquel Sabaté Solà 30ced21b74 Added Portus integration
7 years ago
Jimmy Zelinskie 6c9a131b09 Merge pull request #408 from KeyboardNerd/grpc
7 years ago
Jimmy Zelinskie 74edd854ee Merge pull request #423 from jzelinskie/sleep-updater
7 years ago
Jimmy Zelinskie 0d18a629ca updater: sleep before continuing the lock loop
7 years ago
Jimmy Zelinskie 04847a016d Merge pull request #418 from KeyboardNerd/multiplens
7 years ago
Sida Chen 9561d623c2 featurefmt: use namespace's versionfmt to specify listers
7 years ago
Sida Chen 50437f32a1 featurens: fix detecting duplicated namespaces problem
7 years ago
Jimmy Zelinskie 33c623427f Merge pull request #410 from KeyboardNerd/xforward
7 years ago
Jimmy Zelinskie 6e8d52d020 Merge pull request #416 from tianon/debian-buster
7 years ago
Tianon Gravi de271820a8 Add Debian Buster (10) and update "*stable" aliases
7 years ago
Sida Chen c6f0eaa3c8 api: fix remote addr shows reverse proxy addr problem
7 years ago
Sida Chen 1533dd1d51 vendor: updated vendor dir for grpc v2 api
7 years ago
Sida Chen a4edf38566 api: v2 api with gRPC and gRPC-gateway
7 years ago
Jimmy Zelinskie fec86b6211 Merge pull request #413 from transcedentalia/master
7 years ago
alinar d4a967e6e6 Fixing always revision 0 for ubuntu
7 years ago
Jimmy Zelinskie ce162f5524 Merge pull request #403 from KeyboardNerd/multiplens
7 years ago
Sida Chen f0e21df783 worker: fixed duplicated ns and ns not inherited bug
7 years ago
Jimmy Zelinskie 044425ec07 Merge pull request #407 from swestcott/kubernetes-config-fix
7 years ago
Simon Westcott 303bc9800a Updated Kubernetes config with new db connection string config
7 years ago
Jimmy Zelinskie abd7d2e013 Merge pull request #394 from KeyboardNerd/multiplens
7 years ago
Sida Chen 75d5d40d79 featurens: added multiple namespace testing for namespace detector
7 years ago
Sida Chen bffa6499b7 added support for detect multiple namespaces in a layer
7 years ago
Jimmy Zelinskie c2d8aec157 Merge pull request #382 from caipre/patch-1
7 years ago
Nick Platt aea74550e1 pgsql: Expand layer, namespace column widths
7 years ago

@ -1,4 +1,5 @@
.*
.dockerignore
.travis.yml
*.md
DCO
LICENSE

@ -0,0 +1,47 @@
---
kind: pipeline
name: default
platform:
os: linux
arch: amd64
steps:
- name: publish
pull: default
image: plugins/docker:18.09
settings:
registry: https://registry.nixaid.com
repo: "registry.nixaid.com/${DRONE_REPO_NAMESPACE}/${DRONE_REPO_NAME}"
tags:
- latest
username:
from_secret: docker_username
password:
from_secret: docker_password
# storage_path: /drone/docker
# storage_driver: aufs
# ipv6: false
# debug: true
when:
branch:
- master
event:
- push
- tag
- name: notify
pull: default
image: drillster/drone-email:latest
settings:
from: "Drone CI <noreply@nixaid.com>"
host: mx.nixaid.com
port: 587
subject: "NIXAID Drone Pipeline {{#success build.status}}SUCCESS{{else}}FAILURE{{/success}} Notification"
when:
event:
- push
- tag
status:
- success
- failure

@ -0,0 +1,36 @@
<!--
The Clair developers only support the Clair API.
They do not support or operate any third party client (e.g. clairctl, klar, harbor).
If you are using a third party client, please create issues on their respective repositories.
Are you using a development build of Clair (e.g. quay.io/coreos/clair-git)?
Your problem might be solved by switching to a stable release (e.g. quay.io/coreos/clair).
Issues that do not contain the Environment section will be automatically closed.
If you're making a feature request, please specify "N/A" under the environment section.
Nobody can help you without context.
-->
### Description of Problem / Feature Request
<!--- your content here --->
### Expected Outcome
<!--- your content here --->
### Actual Outcome
<!--- your content here --->
### Environment
- Clair version/image:
- Clair client name/version:
- Host OS:
- Kernel (e.g. `uname -a`):
- Kubernetes version (use `kubectl version`):
- Helm version (use `helm version`):
- Network/Firewall setup:

@ -0,0 +1,4 @@
comment: "This issue is closed because it does not meet our issue template. Please read it."
issueConfigs:
- content:
- "### Environment"

12
.github/stale.yml vendored

@ -0,0 +1,12 @@
daysUntilStale: 60
daysUntilClose: 7
exemptLabels:
- lifecycle/preserve
exemptProjects: true
exemptMilestones: true
staleLabel: lifecycle/stale
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
limitPerRun: 30

@ -1,10 +1,24 @@
language: go
go:
- "1.11.x"
sudo: required
env:
global:
- PATH=$HOME/.local/bin:$PATH
install:
- curl https://glide.sh/get | sh
- mkdir -p $HOME/.local/bin
- curl -o $HOME/.local/bin/prototool -sSL https://github.com/uber/prototool/releases/download/v0.1.0/prototool-$(uname -s)-$(uname -m)
- chmod +x $HOME/.local/bin/prototool
script:
- diff -u <(echo -n) <(gofmt -l -s $(go list -f '{{.Dir}}') | grep -v '/vendor/')
- prototool format -d api/v3/clairpb/clair.proto
- prototool lint api/v3/clairpb/clair.proto
- go test $(glide novendor | grep -v contrib)
dist: trusty
@ -17,11 +31,6 @@ notifications:
matrix:
include:
- addons:
apt:
packages:
- rpm
postgresql: 9.4
- addons:
apt:
packages:

@ -12,17 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
FROM golang:1.10-alpine
VOLUME /config
EXPOSE 6060 6061
FROM golang:1.10-alpine AS build
RUN apk add --no-cache git build-base
ADD . /go/src/github.com/coreos/clair/
WORKDIR /go/src/github.com/coreos/clair/
RUN export CLAIR_VERSION=$(git describe --tag --always --dirty) && \
go build -ldflags "-X github.com/coreos/clair/pkg/version.Version=$CLAIR_VERSION" github.com/coreos/clair/cmd/clair
RUN apk add --no-cache git rpm xz && \
go install -v github.com/coreos/clair/cmd/clair && \
mv /go/bin/clair /clair && \
rm -rf /go /usr/local/go
ENTRYPOINT ["/clair"]
FROM alpine:3.8
COPY --from=build /go/src/github.com/coreos/clair/clair /clair
RUN apk add --no-cache git rpm xz ca-certificates dumb-init
ENTRYPOINT ["/usr/bin/dumb-init", "--", "/clair"]
VOLUME /config
EXPOSE 6060 6061

@ -1,695 +0,0 @@
# Clair v1 API
- [Error Handling](#error-handling)
- [Layers](#layers)
- [POST](#post-layers)
- [GET](#get-layersname)
- [DELETE](#delete-layersname)
- [Namespaces](#namespaces)
- [GET](#get-namespaces)
- [Vulnerabilities](#vulnerabilities)
- [List](#get-namespacesnsnamevulnerabilities)
- [POST](#post-namespacesnamevulnerabilities)
- [GET](#get-namespacesnsnamevulnerabilitiesvulnname)
- [PUT](#put-namespacesnsnamevulnerabilitiesvulnname)
- [DELETE](#delete-namespacesnsnamevulnerabilitiesvulnname)
- [Fixes](#fixes)
- [GET](#get-namespacesnsnamevulnerabilitiesvulnnamefixes)
- [PUT](#put-namespacesnsnamevulnerabilitiesvulnnamefixesfeaturename)
- [DELETE](#delete-namespacesnsnamevulnerabilitiesvulnnamefixesfeaturename)
- [Notifications](#notifications)
- [GET](#get-notificationsname)
- [DELETE](#delete-notificationsname)
## Error Handling
#### Description
Every route can optionally provide an `Error` property on the response object.
The HTTP status code of the response should indicate what type of failure occurred and how the client should react.
#### Client Retry Behavior
| Code | Name | Retry Behavior |
|------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| 400 | Bad Request | The body of the request invalid. The request either must be changed before being retried or depends on another request being processed before it. |
| 404 | Not Found | The requested resource could not be found. The request must be changed before being retried. |
| 422 | Unprocessable Entity | The request body is valid, but unsupported. This request should never be retried. |
| 500 | Internal Server Error | The server encountered an error while processing the request. This request should be retried without change. |
#### Example Response
```http
HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=utf-8
Server: clair
```
```json
{
"Error": {
"Message": "example error message"
}
}
```
## Layers
### POST /layers
#### Description
The POST route for the Layers resource performs the analysis of a Layer from the provided path.
In order to analyze a container image, this route has to be called for each layer that [composes](https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/) it, in the proper order and with the parent relationship specified. For instance, to analyze an image composed of three layers A->B->C, where A is the base layer (i.e. `FROM debian:jessie`), three API calls must be made to this route, from A to C. Also, when analyzing B, A must be set as the parent, then when analyzing C, B must be defined as the parent.
This request blocks for the entire duration of the downloading and indexing of the layer and displays the provided Layer with an updated `IndexByVersion` property.
The Name field must be unique globally. Consequently, using the Blob digest describing the Layer content is not sufficient as Clair won't be able to differentiate two empty filesystem diffs that belong to two different image trees.
The Authorization field is an optional value whose contents will fill the Authorization HTTP Header when requesting the layer via HTTP.
#### Example Request
```http
POST http://localhost:6060/v1/layers HTTP/1.1
```
```json
{
"Layer": {
"Name": "523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6",
"Path": "https://mystorage.com/layers/523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6/layer.tar",
"Headers": {
"Authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE"
},
"ParentName": "140f9bdfeb9784cf8730e9dab5dd12fbd704151cf555ac8cae650451794e5ac2",
"Format": "Docker"
}
}
```
#### Example Response
```http
HTTP/1.1 201 Created
Content-Type: application/json;charset=utf-8
Server: clair
```
```json
{
"Layer": {
"Name": "523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6",
"Path": "https://mystorage.com/layers/523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6/layer.tar",
"Headers": {
"Authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE"
},
"ParentName": "140f9bdfeb9784cf8730e9dab5dd12fbd704151cf555ac8cae650451794e5ac2",
"Format": "Docker",
"IndexedByVersion": 1
}
}
```
### GET /layers/`:name`
#### Description
The GET route for the Layers resource displays a Layer and optionally all of its features and vulnerabilities. For an image composed of three layers A->B->C, calling this route on the third layer (C) will returns all the features and vulnerabilities for the entire image, including the analysis data gathered from the parent layers (A, B). For instance, a feature (and its potential vulnerabilities) detected in the first layer (A) will be shown when querying the third layer (C). On the other hand, a feature detected in the first layer (A) but then removed in either following layers (B, C) will not appear.
#### Query Parameters
| Name | Type | Required | Description |
|-----------------|------|----------|-------------------------------------------------------------------------------|
| features | bool | optional | Displays the list of features indexed in this layer and all of its parents. |
| vulnerabilities | bool | optional | Displays the list of vulnerabilities along with the features described above. |
#### Example Request
```http
GET http://localhost:6060/v1/layers/17675ec01494d651e1ccf81dc9cf63959ebfeed4f978fddb1666b6ead008ed52?features&vulnerabilities HTTP/1.1
```
#### Example Response
```http
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Server: clair
```
```json
{
"Layer": {
"Name": "17675ec01494d651e1ccf81dc9cf63959ebfeed4f978fddb1666b6ead008ed52",
"NamespaceName": "debian:8",
"ParentName": "140f9bdfeb9784cf8730e9dab5dd12fbd704151cf555ac8cae650451794e5ac2",
"IndexedByVersion": 1,
"Features": [
{
"Name": "coreutils",
"NamespaceName": "debian:8",
"Version": "8.23-4",
"Vulnerabilities": [
{
"Name": "CVE-2014-9471",
"NamespaceName": "debian:8",
"Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.",
"Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471",
"Severity": "Low",
"FixedBy": "9.23-5"
}
]
}
]
}
}
```
### DELETE /layers/`:name`
#### Description
The DELETE route for the Layers resource removes a Layer and all of its children from the database.
#### Example Request
```http
DELETE http://localhost:6060/v1/layers/17675ec01494d651e1ccf81dc9cf63959ebfeed4f978fddb1666b6ead008ed52 HTTP/1.1
```
#### Example Response
```http
HTTP/1.1 200 OK
Server: clair
```
## Namespaces
### GET /namespaces
#### Description
The GET route for the Namespaces resource displays a list of namespaces currently being managed.
#### Example Request
```http
GET http://localhost:6060/v1/namespaces HTTP/1.1
```
#### Example Response
```http
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Server: clair
```
```json
{
"Namespaces": [
{ "Name": "debian:8" },
{ "Name": "debian:9" }
]
}
```
## Vulnerabilities
### GET /namespaces/`:nsName`/vulnerabilities
#### Description
The GET route for the Vulnerabilities resource displays the vulnerabilities data for a given namespace.
#### Query Parameters
| Name | Type | Required | Description |
|---------|------|----------|------------------------------------------------------------|
| limit | int | required | Limits the amount of the vunlerabilities data for a given namespace. |
| page | int | required | Displays the specific page of the vunlerabilities data for a given namespace. |
#### Example Request
```http
GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities?limit=2 HTTP/1.1
```
#### Example Response
```http
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Server: clair
```
```json
{
"Vulnerabilities": [
{
"Name": "CVE-1999-1332",
"NamespaceName": "debian:8",
"Description": "gzexe in the gzip package on Red Hat Linux 5.0 and earlier allows local users to overwrite files of other users via a symlink attack on a temporary file.",
"Link": "https://security-tracker.debian.org/tracker/CVE-1999-1332",
"Severity": "Low"
},
{
"Name": "CVE-1999-1572",
"NamespaceName": "debian:8",
"Description": "cpio on FreeBSD 2.1.0, Debian GNU/Linux 3.0, and possibly other operating systems, uses a 0 umask when creating files using the -O (archive) or -F options, which creates the files with mode 0666 and allows local users to read or overwrite those files.",
"Link": "https://security-tracker.debian.org/tracker/CVE-1999-1572",
"Severity": "Low",
"Metadata": {
"NVD": {
"CVSSv2": {
"Score": 2.1,
"Vectors": "AV:L/AC:L/Au:N/C:P/I:N"
}
}
}
}
],
"NextPage":"gAAAAABW1ABiOlm6KMDKYFE022bEy_IFJdm4ExxTNuJZMN0Eycn0Sut2tOH9bDB4EWGy5s6xwATUHiG-6JXXaU5U32sBs6_DmA=="
}
```
### POST /namespaces/`:name`/vulnerabilities
#### Description
The POST route for the Vulnerabilities resource creates a new Vulnerability.
#### Example Request
```http
POST http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities HTTP/1.1
```
```json
{
"Vulnerability": {
"Name": "CVE-2014-9471",
"NamespaceName": "debian:8",
"Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471",
"Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.",
"Severity": "Low",
"Metadata": {
"NVD": {
"CVSSv2": {
"Score": 7.5,
"Vectors": "AV:N/AC:L/Au:N/C:P/I:P"
}
}
},
"FixedIn": [
{
"Name": "coreutils",
"NamespaceName": "debian:8",
"Version": "8.23-1"
}
]
}
}
```
#### Example Response
```http
HTTP/1.1 201 Created
Content-Type: application/json;charset=utf-8
Server: clair
```
```json
{
"Vulnerability": {
"Name": "CVE-2014-9471",
"NamespaceName": "debian:8",
"Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471",
"Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.",
"Severity": "Low",
"Metadata": {
"NVD": {
"CVSSv2": {
"Score": 7.5,
"Vectors": "AV:N/AC:L/Au:N/C:P/I:P"
}
}
},
"FixedIn": [
{
"Name": "coreutils",
"NamespaceName": "debian:8",
"Version": "8.23-1"
}
]
}
}
```
### GET /namespaces/`:nsName`/vulnerabilities/`:vulnName`
#### Description
The GET route for the Vulnerabilities resource displays the current data for a given vulnerability and optionally the features that fix it.
#### Query Parameters
| Name | Type | Required | Description |
|---------|------|----------|------------------------------------------------------------|
| fixedIn | bool | optional | Displays the list of features that fix this vulnerability. |
#### Example Request
```http
GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471?fixedIn HTTP/1.1
```
#### Example Response
```http
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Server: clair
```
```json
{
"Vulnerability": {
"Name": "CVE-2014-9471",
"NamespaceName": "debian:8",
"Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471",
"Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.",
"Severity": "Low",
"Metadata": {
"NVD": {
"CVSSv2": {
"Score": 7.5,
"Vectors": "AV:N/AC:L/Au:N/C:P/I:P"
}
}
},
"FixedIn": [
{
"Name": "coreutils",
"NamespaceName": "debian:8",
"Version": "8.23-1"
}
]
}
}
```
### PUT /namespaces/`:nsName`/vulnerabilities/`:vulnName`
#### Description
The PUT route for the Vulnerabilities resource updates a given Vulnerability.
The "FixedIn" property of the Vulnerability must be empty or missing.
Fixes should be managed by the Fixes resource.
If this vulnerability was inserted by a Fetcher, changes may be lost when the Fetcher updates.
#### Example Request
```http
PUT http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471
```
```json
{
"Vulnerability": {
"Name": "CVE-2014-9471",
"NamespaceName": "debian:8",
"Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471",
"Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.",
"Severity": "Low",
"Metadata": {
"NVD": {
"CVSSv2": {
"Score": 7.5,
"Vectors": "AV:N/AC:L/Au:N/C:P/I:P"
}
}
}
}
}
```
#### Example Response
```http
HTTP/1.1 200 OK
Server: clair
```
```json
{
"Vulnerability": {
"Name": "CVE-2014-9471",
"NamespaceName": "debian:8",
"Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471",
"Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.",
"Severity": "Low",
"Metadata": {
"NVD": {
"CVSSv2": {
"Score": 7.5,
"Vectors": "AV:N/AC:L/Au:N/C:P/I:P"
}
}
}
}
}
```
### DELETE /namespaces/`:nsName`/vulnerabilities/`:vulnName`
#### Description
The DELETE route for the Vulnerabilities resource deletes a given Vulnerability.
If this vulnerability was inserted by a Fetcher, it may be re-inserted when the Fetcher updates.
#### Example Request
```http
GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471 HTTP/1.1
```
#### Example Response
```http
HTTP/1.1 200 OK
Server: clair
```
## Fixes
### GET /namespaces/`:nsName`/vulnerabilities/`:vulnName`/fixes
#### Description
The GET route for the Fixes resource displays the list of Features that fix the given Vulnerability.
#### Example Request
```http
GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471/fixes HTTP/1.1
```
#### Example Response
```http
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Server: clair
```
```json
{
"Features": [
{
"Name": "coreutils",
"NamespaceName": "debian:8",
"Version": "8.23-1"
}
]
}
```
### PUT /namespaces/`:nsName`/vulnerabilities/`:vulnName`/fixes/`:featureName`
#### Description
The PUT route for the Fixes resource updates a Feature that is the fix for a given Vulnerability.
#### Example Request
```http
PUT http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471/fixes/coreutils HTTP/1.1
```
```json
{
"Feature": {
"Name": "coreutils",
"NamespaceName": "debian:8",
"Version": "4.24-9"
}
}
```
#### Example Response
```http
HTTP/1.1 200 OK
Server: clair
```
```json
{
"Feature": {
"Name": "coreutils",
"NamespaceName": "debian:8",
"Version": "4.24-9"
}
}
```
### DELETE /namespaces/`:nsName`/vulnerabilities/`:vulnName`/fixes/`:featureName`
#### Description
The DELETE route for the Fixes resource removes a Feature as fix for the given Vulnerability.
#### Example Request
```http
DELETE http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471/fixes/coreutils
```
#### Example Response
```http
HTTP/1.1 200 OK
Server: clair
```
## Notifications
### GET /notifications/`:name`
#### Description
The GET route for the Notifications resource displays a notification that a Vulnerability has been updated.
This route supports simultaneous pagination for both the `Old` and `New` Vulnerabilities' `OrderedLayersIntroducingVulnerability` which can be extremely long.
The `LayersIntroducingVulnerability` property is deprecated and will eventually be removed from the API.
#### Query Parameters
| Name | Type | Required | Description |
|-------|--------|----------|---------------------------------------------------------------------------------------------------------------|
| page | string | optional | Displays the specific page of the "LayersIntroducingVulnerability" property on New and Old vulnerabilities. |
| limit | int | optional | Limits the amount of results in the "LayersIntroducingVulnerability" property on New and Old vulnerabilities. |
#### Example Request
```http
GET http://localhost:6060/v1/notifications/ec45ec87-bfc8-4129-a1c3-d2b82622175a?limit=2 HTTP/1.1
```
#### Example Response
```http
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Server: clair
```
```json
{
"Notification": {
"Name": "ec45ec87-bfc8-4129-a1c3-d2b82622175a",
"Created": "1456247389",
"Notified": "1456246708",
"Limit": 2,
"Page": "gAAAAABWzJaC2JCH6Apr_R1f2EkjGdibnrKOobTcYXBWl6t0Cw6Q04ENGIymB6XlZ3Zi0bYt2c-2cXe43fvsJ7ECZhZz4P8C8F9efr_SR0HPiejzQTuG0qAzeO8klogFfFjSz2peBvgP",
"NextPage": "gAAAAABWzJaCTyr6QXP2aYsCwEZfWIkU2GkNplSMlTOhLJfiR3LorBv8QYgEIgyOvZRmHQEzJKvkI6TP2PkRczBkcD17GE89btaaKMqEX14yHDgyfQvdasW1tj3-5bBRt0esKi9ym5En",
"New": {
"Vulnerability": {
"Name": "CVE-TEST",
"NamespaceName": "debian:8",
"Description": "New CVE",
"Severity": "Low",
"FixedIn": [
{
"Name": "grep",
"NamespaceName": "debian:8",
"Version": "2.25"
}
]
},
"OrderedLayersIntroducingVulnerability": [
{
"Index": 1,
"LayerName": "523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6"
},
{
"Index": 2,
"LayerName": "3b59c795b34670618fbcace4dac7a27c5ecec156812c9e2c90d3f4be1916b12d"
}
],
"LayersIntroducingVulnerability": [
"523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6",
"3b59c795b34670618fbcace4dac7a27c5ecec156812c9e2c90d182371916b12d"
]
},
"Old": {
"Vulnerability": {
"Name": "CVE-TEST",
"NamespaceName": "debian:8",
"Description": "New CVE",
"Severity": "Low",
"FixedIn": []
},
"OrderedLayersIntroducingVulnerability": [
{
"Index": 1,
"LayerName": "523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6"
},
{
"Index": 2,
"LayerName": "3b59c795b34670618fbcace4dac7a27c5ecec156812c9e2c90d3f4be1916b12d"
}
],
"LayersIntroducingVulnerability": [
"3b59c795b34670618fbcace4dac7a27c5ecec156812c9e2c90d3f4be1916b12d",
"523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6"
]
}
}
}
```
### DELETE /notifications/`:name`
#### Description
The delete route for the Notifications resource marks a Notification as read.
If a notification is not marked as read, Clair will continue to notify the provided endpoints.
The time at which this Notification was marked as read can be seen in the `Notified` property of the response GET route for Notification.
#### Example Request
```http
DELETE http://localhost:6060/v1/notification/ec45ec87-bfc8-4129-a1c3-d2b82622175a HTTP/1.1
```
#### Example Response
```http
HTTP/1.1 200 OK
Server: clair
```

@ -0,0 +1,93 @@
# Understanding drivers, their data sources, and creating your own
Clair is organized into many different software components all of which are dynamically registered at compile time.
All of these components can be found in the `ext/` directory.
## Driver Types
| Driver Type | Functionality | Example |
|--------------|------------------------------------------------------------------------------------|---------------|
| featurefmt | parses features of a particular format out of a layer | apk |
| featurens | identifies whether a particular namespaces is applicable to a layer | alpine 3.5 |
| imagefmt | determines the location of the root filesystem location for a layer | docker |
| notification | implements the transport used to notify of vulnerability changes | webhook |
| versionfmt | parses and compares version strings | rpm |
| vulnmdsrc | fetches vulnerability metadata and appends them to vulnerabilities being processed | nvd |
| vulnsrc | fetches vulnerabilities for a set of namespaces | alpine-sec-db |
## Data Sources for the built-in drivers
| Data Source | Data Collected | Format | License |
|------------------------------------|--------------------------------------------------------------------------|--------|-----------------|
| [Debian Security Bug Tracker] | Debian 6, 7, 8, unstable namespaces | [dpkg] | [Debian] |
| [Ubuntu CVE Tracker] | Ubuntu 12.04, 12.10, 13.04, 14.04, 14.10, 15.04, 15.10, 16.04 namespaces | [dpkg] | [GPLv2] |
| [Red Hat Security Data] | CentOS 5, 6, 7 namespaces | [rpm] | [CVRF] |
| [Oracle Linux Security Data] | Oracle Linux 5, 6, 7 namespaces | [rpm] | [CVRF] |
| [Amazon Linux Security Advisories] | Amazon Linux 2018.03, 2 namespaces | [rpm] | [MIT-0] |
| [SUSE OVAL Descriptions] | openSUSE, SUSE Linux Enterprise namespaces | [rpm] | [CC-BY-NC-4.0] |
| [Alpine SecDB] | Alpine 3.3, Alpine 3.4, Alpine 3.5 namespaces | [apk] | [MIT] |
| [NIST NVD] | Generic Vulnerability Metadata | N/A | [Public Domain] |
[Debian Security Bug Tracker]: https://security-tracker.debian.org/tracker
[Ubuntu CVE Tracker]: https://launchpad.net/ubuntu-cve-tracker
[Red Hat Security Data]: https://www.redhat.com/security/data/metrics
[Oracle Linux Security Data]: https://linux.oracle.com/security/
[SUSE OVAL Descriptions]: https://www.suse.com/de-de/support/security/oval/
[Amazon Linux Security Advisories]: https://alas.aws.amazon.com/
[NIST NVD]: https://nvd.nist.gov
[dpkg]: https://en.wikipedia.org/wiki/dpkg
[rpm]: http://www.rpm.org
[Debian]: https://www.debian.org/license
[GPLv2]: https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
[CVRF]: http://www.icasi.org/cvrf-licensing/
[Public Domain]: https://nvd.nist.gov/faq
[Alpine SecDB]: http://git.alpinelinux.org/cgit/alpine-secdb/
[apk]: http://git.alpinelinux.org/cgit/apk-tools/
[MIT]: https://gist.github.com/jzelinskie/6da1e2da728424d88518be2adbd76979
[MIT-0]: https://spdx.org/licenses/MIT-0.html
[CC-BY-NC-4.0]: https://creativecommons.org/licenses/by-nc/4.0/]
## Adding new drivers
In order to allow programmers to add additional behavior, Clair follows a pattern that Go programmers may recognize from the standard `database/sql` library.
Each Driver Type defines an interface that must be implemented by drivers.
```go
type DriverInterface interface {
Action() error
}
func RegisterDriver(name, DriverInterface) { ... }
```
Create a new Go package containing an implementation of the driver interface.
In the source file that implements this custom interface, create an `init()` function that registers the driver.
```go
func init() {
drivers.RegisterDriver("mydrivername", myDriverImplementation{})
}
// This line causes the Go compiler to enforce that myDriverImplementation
// implements the the DriverInterface at compile time.
var _ drivers.DriverInterface = myDriverImplementation{}
type myDriverImplementation struct{}
func (d myDriverImplementation) Action() error {
fmt.Println("Hello, Clair!")
return nil
}
```
The final step is to import the new driver in `main.go` as `_` in order ensure that the `init()` function executes, thus registering your driver.
```go
import (
...
_ "github.com/you/yourclairdriver"
)
```
If you believe what you've created can benefit others outside of your organization, please consider open sourcing it and creating a pull request to get it included by default.

@ -8,6 +8,12 @@ This document tracks projects that integrate with Clair. [Join the community](ht
[Dockyard](https://github.com/Huawei/dockyard): an open source container registry with Clair integration.
[Yair](https://github.com/yfoelling/yair): a lightweight command-line for working with clair with many different outputs. Mainly designed for usage in a CI Job.
[Claircli](https://github.com/joelee2012/claircli): A simple cmd tool to interact with CoreOS Clair.
[Paclair](https://github.com/yebinama/paclair): a Python3 CLI tool to interact with Clair (easily configurable to access private registries).
[Clairctl](https://github.com/jgsqware/clairctl): a lightweight command-line tool for working locally with Clair and generate HTML report.
[Clair-SQS](https://github.com/zalando-incubator/clair-sqs): a container containing Clair and additional processes that integrate Clair with [Amazon SQS][sqs].
@ -20,5 +26,10 @@ This document tracks projects that integrate with Clair. [Join the community](ht
[check_openvz_mirror_with_clair](https://github.com/FastVPSEestiOu/check_openvz_mirror_with_clair): a tool to use Clair to analyze OpenVZ templates
[Portus](http://port.us.org/features/6_security_scanning.html#coreos-clair): an authorization service and frontend for Docker registry (v2).
[clair-scanner](https://github.com/arminc/clair-scanner): a project similar to 'analyze-local-images' with a whitelisting feature
[sqs]: https://aws.amazon.com/sqs/
[clair-singularity](https://github.com/dctrud/clair-singularity): a command-line tool to scan [Singularity](http://singularity.lbl.gov/) container images using Clair.

@ -1,13 +1,14 @@
# Notifications
Notifications are a way for Clair to inform an endpoint that changes to tracked vulnerabilities have occurred.
Notifications are a way for Clair to inform another service that changes to tracked vulnerabilities have occurred.
Because changes to vulnerabilities also contain the set of affected images, Clair sends only the name of the notification to another service, then depends on that service read and mark the notification as read using Clair's API.
Because notification data can require pagination, Clair should only send the name of a notification.
It is expected that the receiving endpoint calls the Clair API for reading notifications and marking them as read after being notified.
If the notification is never marked as read, Clair will continue attempting to send the same notification to the endpoint indefinitely.
If a notification is not marked as read, Clair will resend notifications at a configured interval.
## Webhook
# Webhook
Webhook is an out-of-the-box notifier that sends the following JSON object via an HTTP POST:
Notifications are an extensible component of Clair, but out of the box Clair supports [webhooks].
The webhooks look like the following:
```json
{
@ -17,11 +18,7 @@ Webhook is an out-of-the-box notifier that sends the following JSON object via a
}
```
## Custom Notifiers
If you're interested in adding your own notification senders, read the documentation on [adding new drivers].
Clair can also be compiled with custom notifiers by importing them in `main.go`.
Custom notifiers are any Go package that implements the `Notifier` interface and registers themselves with the `notifier` package.
Notifiers are registered in [init()] similar to drivers for Go's standard [database/sql] package.
[init()]: https://golang.org/doc/effective_go.html#init
[database/sql]: https://godoc.org/database/sql
[webhooks]: https://en.wikipedia.org/wiki/Webhook
[adding new drivers]: /Documentation/drivers-and-data-sources.md#adding-new-drivers

@ -0,0 +1,24 @@
# Presentations
This document tracks projects that integrate with Clair. [Join the community](https://github.com/coreos/clair/), and help us keep the list up-to-date.
## Clair v3
Coming soon...
## Clair v2
| Title | Event | Video/Slides |
|---------------------------------------------------------------------------|-------------------------------|--------------------------------------------------------|
| Clair: The Container Image Security Analyzer | [ContainerDays Boston 2016] | [YouTube][CDB2016YouTube] [SlideShare][CDB2016Slides] |
| Identifying Common Vulnerabilities and Exposures in Containers with Clair | [CoreOS Fest 2016] | [YouTube](https://www.youtube.com/watch?v=YDCa51BK2q0) |
| Clair: A Container Image Security Analyzer | [Microservices NYC] | [YouTube](https://www.youtube.com/watch?v=ynwKi2yhIX4) |
| Clair: A Container Image Security Analyzer | [Container Orchestration NYC] | [YouTube](https://www.youtube.com/watch?v=wTfCOUDNV_M) |
[ContainerDays Boston 2016]: http://dynamicinfradays.org/events/2016-boston/
[CDB2016YouTube]: https://www.youtube.com/watch?v=Kri67PtPv6s
[CDB2016Slides]: https://www.slideshare.net/CoreOS_Slides/clair-a-container-image-security-analyzer-61197704
[CoreOS Fest 2016]: https://coreos.com/fest/#2016
[Microservices NYC]: https://www.meetup.com/Microservices-NYC/events/230023492/
[Container Orchestration NYC]: https://www.meetup.com/Kubernetes-Cloud-Native-New-York/events/229779466/

@ -4,5 +4,4 @@ This document tracks people and use cases for Clair in production. [Join the com
## [Quay.io](https://quay.io/)
Quay is CoreOS' enterprise-ready container registry. It displays the results of a Clair security scan on each hosted image's page.
Quay.io is hosted and enterprise-ready container registry. It displays the results of a Clair security scan for each image uploaded to the registry.

@ -0,0 +1,117 @@
# Running Clair
The following document outlines possible ways to deploy Clair both on your local machine and to a cluster of machines.
## Official Container Repositories
Clair is officially packaged and released as a container.
* [quay.io/coreos/clair] - Stable releases
* [quay.io/coreos/clair-jwt] - Stable releases with an embedded instance of [jwtproxy]
* [quay.io/coreos/clair-git] - Development releases
[quay.io/coreos/clair]: https://quay.io/repository/coreos/clair
[jwtproxy]: https://github.com/coreos/jwtproxy
[quay.io/coreos/clair-jwt]: https://quay.io/repository/coreos/clair-jwt
[quay.io/coreos/clair-git]: https://quay.io/repository/coreos/clair-git
## Common Architecture
### Registry Integration
Clair can be integrated directly into a container registry such that the registry is responsible for interacting with Clair on behalf of the user.
This type of setup avoids the manual scanning of images and creates a sensible location to which Clair's vulnerability notifications can be propagated.
The registry can also be used for authorization to avoid sharing vulnerability information about images to which one might not have access.
![Simple Clair Diagram](https://cloud.githubusercontent.com/assets/343539/21630809/c1adfbd2-d202-11e6-9dfe-9024139d0a28.png)
### CI/CD Integration
Clair can be integrated into a CI/CD pipeline such that when a container image is produced, the step after pushing the image to a registry is to compose a request for Clair to scan that particular image.
This type of integration is more flexible, but relies on additional components to be setup in order to secure.
## Deployment Strategies
**NOTE:** These instructions demonstrate running HEAD and not stable versions.
The following are community supported instructions to run Clair in a variety of ways.
A [PostgreSQL 9.4+] database instance is required for all instructions.
[PostgreSQL 9.4+]: https://www.postgresql.org
### Cluster
#### Kubernetes (Helm)
If you don't have a local Kubernetes cluster already, check out [minikube].
This assumes you've already ran `helm init`, you have access to a currently running instance of Tiller and that you are running the latest version of helm.
[minikube]: https://github.com/kubernetes/minikube
```
git clone https://github.com/coreos/clair
cd clair/contrib/helm
cp clair/values.yaml ~/my_custom_values.yaml
vi ~/my_custom_values.yaml
helm dependency update clair
helm install clair -f ~/my_custom_values.yaml
```
### Local
#### Docker Compose
```sh
$ curl -L https://raw.githubusercontent.com/coreos/clair/master/contrib/compose/docker-compose.yml -o $HOME/docker-compose.yml
$ mkdir $HOME/clair_config
$ curl -L https://raw.githubusercontent.com/coreos/clair/master/config.yaml.sample -o $HOME/clair_config/config.yaml
$ $EDITOR $HOME/clair_config/config.yaml # Edit database source to be postgresql://postgres:password@postgres:5432?sslmode=disable
$ docker-compose -f $HOME/docker-compose.yml up -d
```
Docker Compose may start Clair before Postgres which will raise an error.
If this error is raised, manually execute `docker-compose start clair`.
#### Docker
```sh
$ mkdir $PWD/clair_config
$ curl -L https://raw.githubusercontent.com/coreos/clair/master/config.yaml.sample -o $PWD/clair_config/config.yaml
$ docker run -d -e POSTGRES_PASSWORD="" -p 5432:5432 postgres:9.6
$ docker run --net=host -d -p 6060-6061:6060-6061 -v $PWD/clair_config:/config quay.io/coreos/clair-git:latest -config=/config/config.yaml
```
#### Source
To build Clair, you need the latest stable version of [Go] and a working [Go environment].
In addition, Clair requires some additional binaries be installed on the system [$PATH] as runtime dependencies:
* [git]
* [rpm]
* [xz]
[Go]: https://github.com/golang/go/releases
[Go environment]: https://golang.org/doc/code.html
[git]: https://git-scm.com
[rpm]: http://www.rpm.org
[xz]: http://tukaani.org/xz
[$PATH]: https://en.wikipedia.org/wiki/PATH_(variable)
```sh
$ go get github.com/coreos/clair
$ go install github.com/coreos/clair/cmd/clair
$ $EDITOR config.yaml # Add the URI for your postgres database
$ ./$GOPATH/bin/clair -config=config.yaml
```
## Troubleshooting
### I just started up Clair and nothing appears to be working, what's the deal?
During the first run, Clair will bootstrap its database with vulnerability data from the configured data sources.
It can take several minutes before the database has been fully populated, but once this data is stored in the database, subsequent updates will take far less time.
### I'm seeing Linux kernel vulnerabilities in my image, that doesn't make any sense since containers share the host kernel!
Many container base images using Linux distributions as a foundation will install dummy kernel packages that do nothing but satisfy their package manager's dependency requirements.
The Clair developers have taken the stance that Clair should not filter results, providing the most accurate data as possible to user interfaces that can then apply filters that make sense for their users.

@ -0,0 +1,15 @@
# Terminology
## Container
- *Container* - the execution of an image
- *Image* - a set of tarballs that contain the filesystem contents and run-time metadata of a container
- *Layer* - one of the tarballs used in the composition of an image, often expressed as a filesystem delta from another layer
## Specific to Clair
- *Ancestry* - the Clair-internal representation of an Image
- *Feature* - anything that when present in a filesystem could be an indication of a *vulnerability* (e.g. the presence of a file or an installed software package)
- *Feature Namespace* (featurens) - a context around *features* and *vulnerabilities* (e.g. an operating system or a programming language)
- *Vulnerability Source* (vulnsrc) - the component of Clair that tracks upstream vulnerability data and imports them into Clair's database
- *Vulnerability Metadata Source* (vulnmdsrc) - the component of Clair that tracks upstream vulnerability metadata and associates them with vulnerabilities in Clair's database

@ -1,3 +1,6 @@
[![Build Status](https://drone.nixaid.com/api/badges/arno/clair/status.svg)](https://drone.nixaid.com/arno/clair)
----
# Clair
[![Build Status](https://api.travis-ci.org/coreos/clair.svg?branch=master "Build Status")](https://travis-ci.org/coreos/clair)
@ -11,12 +14,12 @@ Please use [releases] instead of the `master` branch in order to get stable bina
![Clair Logo](https://cloud.githubusercontent.com/assets/343539/21630811/c5081e5c-d202-11e6-92eb-919d5999c77a.png)
Clair is an open source project for the static analysis of vulnerabilities in application containers (currently including [appc] and [docker]).
Clair is an open source project for the [static analysis] of vulnerabilities in application containers (currently including [appc] and [docker]).
1. In regular intervals, Clair ingests vulnerability metadata from a configured set of sources and stores it in the database.
2. Clients use the Clair API to index their container images; this parses a list of installed _source packages_ and stores them in the database.
3. Clients use the Clair API to query the database; correlating data is done in real time, rather than a cached result that needs re-scanning.
4. When updates to vulnerability metadata occur, a webhook containg the affected images can be configured to page or block deployments.
2. Clients use the Clair API to index their container images; this creates a list of _features_ present in the image and stores them in the database.
3. Clients use the Clair API to query the database for vulnerabilities of a particular image; correlating vulnerabilities and features is done for each request, avoiding the need to rescan images.
4. When updates to vulnerability metadata occur, a notification can be sent to alert systems that a change has occured.
Our goal is to enable a more transparent view of the security of container-based infrastructure.
Thus, the project was named `Clair` after the French term which translates to *clear*, *bright*, *transparent*.
@ -24,199 +27,35 @@ Thus, the project was named `Clair` after the French term which translates to *c
[appc]: https://github.com/appc/spec
[docker]: https://github.com/docker/docker/blob/master/image/spec/v1.2.md
[releases]: https://github.com/coreos/clair/releases
[static analysis]: https://en.wikipedia.org/wiki/Static_program_analysis
## When would I use Clair?
* You've found an image by searching the internet and want to determine if it's safe enough for you to use in production.
* You're regularly deploying into a containerized production environment and want operations to alert or block deployments on insecure software.
## Documentation
* [The CoreOS website] has a rendered version of the latest stable documentation
* [Inside the Documentation directory] is the source markdown files for documentation
[The CoreOS website]: https://coreos.com/clair/docs/latest/
[Inside the Documentation directory]: /Documentation
## Deploying Clair
### Container Repositories
Clair is officially packaged and released as a container.
* [quay.io/coreos/clair] - Stable releases
* [quay.io/coreos/clair-jwt] - Stable releases with an embedded instance of [jwtproxy]
* [quay.io/coreos/clair-git] - Development releases
[quay.io/coreos/clair]: https://quay.io/repository/coreos/clair
[jwtproxy]: https://github.com/coreos/jwtproxy
[quay.io/coreos/clair-jwt]: https://quay.io/repository/coreos/clair-jwt
[quay.io/coreos/clair-git]: https://quay.io/repository/coreos/clair-git
### Commercially Supported
Clair is professionally supported as a data source for the [Quay] Security Scanning feature.
The setup documentation for using Clair for this environment can be found on the [Quay documentation] on the [CoreOS] website.
Be sure to adjust the version of the documentation to the version of Quay being used in your deployment.
[Quay]: https://quay.io
[Quay documentation]: https://coreos.com/quay-enterprise/docs/latest/clair.html
[CoreOS]: https://coreos.com
### Community Supported
**NOTE:** These instructions demonstrate running HEAD and not stable versions.
The following are community supported instructions to run Clair in a variety of ways.
A database instance is required for all instructions.
Clair currently supports and tests against:
* [Postgres] 9.4
* [Postgres] 9.5
* [Postgres] 9.6
[Postgres]: https://www.postgresql.org
#### Kubernetes
If you don't have a local Kubernetes cluster already, check out [minikube].
[minikube]: https://github.com/kubernetes/minikube
```
git clone https://github.com/coreos/clair
cd clair/contrib/k8s
kubectl create secret generic clairsecret --from-file=./config.yaml
kubectl create -f clair-kubernetes.yaml
```
#### Docker Compose
```sh
$ curl -L https://raw.githubusercontent.com/coreos/clair/master/docker-compose.yml -o $HOME/docker-compose.yml
$ mkdir $HOME/clair_config
$ curl -L https://raw.githubusercontent.com/coreos/clair/master/config.example.yaml -o $HOME/clair_config/config.yaml
$ $EDITOR $HOME/clair_config/config.yaml # Edit database source to be postgresql://postgres:password@postgres:5432?sslmode=disable
$ docker-compose -f $HOME/docker-compose.yml up -d
```
Docker Compose may start Clair before Postgres which will raise an error.
If this error is raised, manually execute `docker-compose start clair`.
#### Docker
```sh
$ mkdir $PWD/clair_config
$ curl -L https://raw.githubusercontent.com/coreos/clair/master/config.example.yaml -o $PWD/clair_config/config.yaml
$ docker run -d -e POSTGRES_PASSWORD="" -p 5432:5432 postgres:9.6
$ docker run -d -p 6060-6061:6060-6061 -v $PWD/clair_config:/config quay.io/coreos/clair-git:latest -config=/config/config.yaml
```
#### Source
To build Clair, you need to latest stable version of [Go] and a working [Go environment].
In addition, Clair requires some additional binaries be installed on the system [$PATH] as runtime dependencies:
* [git]
* [rpm]
* [xz]
[Go]: https://github.com/golang/go/releases
[Go environment]: https://golang.org/doc/code.html
[git]: https://git-scm.com
[rpm]: http://www.rpm.org
[xz]: http://tukaani.org/xz
[$PATH]: https://en.wikipedia.org/wiki/PATH_(variable)
```sh
$ go get github.com/coreos/clair
$ go install github.com/coreos/clair/cmd/clair
$ $EDITOR config.yaml # Add the URI for your postgres database
$ ./$GOPATH/bin/clair -config=config.yaml
```
## Frequently Asked Questions
### Who's using Clair?
You can find [production users] and third party [integrations] documented in their respective pages of the local documentation.
[production users]: https://github.com/coreos/clair/blob/master/Documentation/production-users.md
[integrations]: https://github.com/coreos/clair/blob/master/Documentation/integrations.md
### What do you mean by static analysis?
There are two major ways to perform analysis of programs: [Static Analysis] and [Dynamic Analysis].
Clair has been designed to perform *static analysis*; containers never need to be executed.
Rather, the filesystem of the container image is inspected and *features* are indexed into a database.
By indexing the features of an image into the database, images only need to be rescanned when new *detectors* are added.
[Static Analysis]: https://en.wikipedia.org/wiki/Static_program_analysis
[Dynamic Analysis]: https://en.wikipedia.org/wiki/Dynamic_program_analysis
### What data sources does Clair currently support?
| Data Source | Data Collected | Format | License |
|-------------------------------|--------------------------------------------------------------------------|--------|-----------------|
| [Debian Security Bug Tracker] | Debian 6, 7, 8, unstable namespaces | [dpkg] | [Debian] |
| [Ubuntu CVE Tracker] | Ubuntu 12.04, 12.10, 13.04, 14.04, 14.10, 15.04, 15.10, 16.04 namespaces | [dpkg] | [GPLv2] |
| [Red Hat Security Data] | CentOS 5, 6, 7 namespaces | [rpm] | [CVRF] |
| [Oracle Linux Security Data] | Oracle Linux 5, 6, 7 namespaces | [rpm] | [CVRF] |
| [Alpine SecDB] | Alpine 3.3, Alpine 3.4, Alpine 3.5 namespaces | [apk] | [MIT] |
| [NIST NVD] | Generic Vulnerability Metadata | N/A | [Public Domain] |
[Debian Security Bug Tracker]: https://security-tracker.debian.org/tracker
[Ubuntu CVE Tracker]: https://launchpad.net/ubuntu-cve-tracker
[Red Hat Security Data]: https://www.redhat.com/security/data/metrics
[Oracle Linux Security Data]: https://linux.oracle.com/security/
[NIST NVD]: https://nvd.nist.gov
[dpkg]: https://en.wikipedia.org/wiki/dpkg
[rpm]: http://www.rpm.org
[Debian]: https://www.debian.org/license
[GPLv2]: https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
[CVRF]: http://www.icasi.org/cvrf-licensing/
[Public Domain]: https://nvd.nist.gov/faq
[Alpine SecDB]: http://git.alpinelinux.org/cgit/alpine-secdb/
[apk]: http://git.alpinelinux.org/cgit/apk-tools/
[MIT]: https://gist.github.com/jzelinskie/6da1e2da728424d88518be2adbd76979
### What do most deployments look like?
From a high-level, most deployments integrate with the registry workflow rather than manual API usage by a human.
They typically take up a form similar to the following diagram:
![Simple Clair Diagram](https://cloud.githubusercontent.com/assets/343539/21630809/c1adfbd2-d202-11e6-9dfe-9024139d0a28.png)
### I just started up Clair and nothing appears to be working, what's the deal?
During the first run, Clair will bootstrap its database with vulnerability data from the configured data sources.
It can take several minutes before the database has been fully populated, but once this data is stored in the database, subsequent updates will take far less time.
## Getting Started
### What terminology do I need to understand to work with Clair internals?
* Learn [the terminology] and about the [drivers and data sources] that power Clair
* Watch [presentations] on the high-level goals and design of Clair
* Follow instructions to get Clair [up and running]
* Explore [the API] on SwaggerHub
* Discover third party [integrations] that help integrate Clair with your infrastructure
* Read the rest of the documentation on the [CoreOS website] or in the [Documentation directory]
- *Image* - a tarball of the contents of a container
- *Layer* - an *appc* or *Docker* image that may or may not be dependent on another image
- *Feature* - anything that when present could be an indication of a *vulnerability* (e.g. the presence of a file or an installed software package)
- *Feature Namespace* - a context around *features* and *vulnerabilities* (e.g. an operating system)
- *Vulnerability Updater* - a Go package that tracks upstream vulnerability data and imports them into Clair
- *Vulnerability Metadata Appender* - a Go package that tracks upstream vulnerability metadata and appends them into vulnerabilities managed by Clair
[the terminology]: /Documentation/terminology.md
[drivers and data sources]: /Documentation/drivers-and-data-sources.md
[presentations]: /Documentation/presentations.md
[up and running]: /Documentation/running-clair.md
[the API]: https://app.swaggerhub.com/apis/coreos/clair/3.0
[integrations]: /Documentation/integrations.md
[CoreOS website]: https://coreos.com/clair/docs/latest/
[Documentation directory]: /Documentation
### How can I customize Clair?
## Contact
The major components of Clair are all programmatically extensible in the same way Go's standard [database/sql] package is extensible.
Everything extensible is located in the `ext` directory.
- IRC: #[clair](irc://irc.freenode.org:6667/#clair) on freenode.org
- Bugs: [issues](https://github.com/coreos/clair/issues)
Custom behavior can be accomplished by creating a package that contains a type that implements an interface declared in Clair and registering that interface in [init()].
To expose the new behavior, unqualified imports to the package must be added in your own custom [main.go], which should then start Clair using `Boot(*config.Config)`.
## Contributing
[database/sql]: https://godoc.org/database/sql
[init()]: https://golang.org/doc/effective_go.html#init
[main.go]: https://github.com/coreos/clair/blob/master/cmd/clair/main.go
See [CONTRIBUTING](.github/CONTRIBUTING.md) for details on submitting patches and the contribution workflow.
### Are there any public presentations on Clair?
## License
- _Clair: The Container Image Security Analyzer @ ContainerDays Boston 2016_ - [Event](http://dynamicinfradays.org/events/2016-boston/) [Video](https://www.youtube.com/watch?v=Kri67PtPv6s) [Slides](https://docs.google.com/presentation/d/1ExQGZs-pQ56TpW_ifcUl2l_ml87fpCMY6-wdug87OFU)
- _Identifying Common Vulnerabilities and Exposures in Containers with Clair @ CoreOS Fest 2016_ - [Event](https://coreos.com/fest/) [Video](https://www.youtube.com/watch?v=YDCa51BK2q0) [Slides](https://docs.google.com/presentation/d/1pHSI_5LcjnZzZBPiL1cFTZ4LvhzKtzh86eE010XWNLY)
- _Clair: A Container Image Security Analyzer @ Microservices NYC_ - [Event](https://www.meetup.com/Microservices-NYC/events/230023492/) [Video](https://www.youtube.com/watch?v=ynwKi2yhIX4) [Slides](https://docs.google.com/presentation/d/1ly9wQKQIlI7rlb0JNU1_P-rPDHU4xdRCCM3rxOdjcgc)
- _Clair: A Container Image Security Analyzer @ Container Orchestration NYC_ - [Event](https://www.meetup.com/Container-Orchestration-NYC/events/229779466/) [Video](https://www.youtube.com/watch?v=wTfCOUDNV_M) [Slides](https://docs.google.com/presentation/d/1ly9wQKQIlI7rlb0JNU1_P-rPDHU4xdRCCM3rxOdjcgc)
Clair is under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.

@ -8,8 +8,12 @@ The [milestones defined in GitHub](https://github.com/coreos/clair/milestones) r
The roadmap below outlines new features that will be added to Clair, and while subject to change, define what future stable will look like.
- Support multiple namespaces per image
- This enables language-level package managers (e.g. npm, pip)
- This enables language-level package managers (e.g. npm, pip) in the future
- Take advantage of OCI/Docker content-addressiblity to avoid duplicated work
- This simplifies the amount of work required for an offline clair in the future
- Support mappings between source packages and binary packages
- Versioned detectors that are present in API results
- This will enable clients to determine when images need to be reindexed
- gRPC API that works on sets of layers rather than individual layers
- Structured logging in JSON
- Improve coverage and readability of documentation
- Decouple the project from Postgres
- gRPC API supporting direct uploads of images
- Support operating Clair without internet access

@ -0,0 +1,145 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clair
import (
"context"
log "github.com/sirupsen/logrus"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurefmt"
"github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/ext/imagefmt"
)
// AnalyzeError represents an failure when analyzing layer or constructing
// ancestry.
type AnalyzeError string
func (e AnalyzeError) Error() string {
return string(e)
}
var (
// StorageError represents an analyze error caused by the storage
StorageError = AnalyzeError("failed to query the database.")
// RetrieveBlobError represents an analyze error caused by failure of
// downloading or extracting layer blobs.
RetrieveBlobError = AnalyzeError("failed to download layer blob.")
// ExtractBlobError represents an analyzer error caused by failure of
// extracting a layer blob by imagefmt.
ExtractBlobError = AnalyzeError("failed to extract files from layer blob.")
// FeatureDetectorError is an error caused by failure of feature listing by
// featurefmt.
FeatureDetectorError = AnalyzeError("failed to scan feature from layer blob files.")
// NamespaceDetectorError is an error caused by failure of namespace
// detection by featurens.
NamespaceDetectorError = AnalyzeError("failed to scan namespace from layer blob files.")
)
// AnalyzeLayer retrieves the clair layer with all extracted features and namespaces.
// If a layer is already scanned by all enabled detectors in the Clair instance, it returns directly.
// Otherwise, it re-download the layer blob and scan the features and namespaced again.
func AnalyzeLayer(ctx context.Context, store database.Datastore, blobSha256 string, blobFormat string, downloadURI string, downloadHeaders map[string]string) (*database.Layer, error) {
layer, found, err := database.FindLayerAndRollback(store, blobSha256)
logFields := log.Fields{"layer.Hash": blobSha256}
if err != nil {
log.WithError(err).WithFields(logFields).Error("failed to find layer in the storage")
return nil, StorageError
}
var scannedBy []database.Detector
if found {
scannedBy = layer.By
}
// layer will be scanned by detectors not scanned the layer already.
toScan := database.DiffDetectors(EnabledDetectors(), scannedBy)
if len(toScan) != 0 {
log.WithFields(logFields).Debug("scan layer blob not already scanned")
newLayerScanResult := &database.Layer{Hash: blobSha256, By: toScan}
blob, err := retrieveLayerBlob(ctx, downloadURI, downloadHeaders)
if err != nil {
log.WithError(err).WithFields(logFields).Error("failed to retrieve layer blob")
return nil, RetrieveBlobError
}
defer func() {
if err := blob.Close(); err != nil {
log.WithFields(logFields).Error("failed to close layer blob reader")
}
}()
files := append(featurefmt.RequiredFilenames(toScan), featurens.RequiredFilenames(toScan)...)
fileMap, err := imagefmt.Extract(blobFormat, blob, files)
if err != nil {
log.WithFields(logFields).WithError(err).Error("failed to extract layer blob")
return nil, ExtractBlobError
}
newLayerScanResult.Features, err = featurefmt.ListFeatures(fileMap, toScan)
if err != nil {
log.WithFields(logFields).WithError(err).Error("failed to detect features")
return nil, FeatureDetectorError
}
newLayerScanResult.Namespaces, err = featurens.Detect(fileMap, toScan)
if err != nil {
log.WithFields(logFields).WithError(err).Error("failed to detect namespaces")
return nil, NamespaceDetectorError
}
if err = saveLayerChange(store, newLayerScanResult); err != nil {
log.WithFields(logFields).WithError(err).Error("failed to store layer change")
return nil, StorageError
}
layer = database.MergeLayers(layer, newLayerScanResult)
} else {
log.WithFields(logFields).Debug("found scanned layer blob")
}
return layer, nil
}
// EnabledDetectors retrieves a list of all detectors installed in the Clair
// instance.
func EnabledDetectors() []database.Detector {
return append(featurefmt.ListListers(), featurens.ListDetectors()...)
}
// RegisterConfiguredDetectors populates the database with registered detectors.
func RegisterConfiguredDetectors(store database.Datastore) {
if err := database.PersistDetectorsAndCommit(store, EnabledDetectors()); err != nil {
panic("failed to initialize Clair analyzer")
}
}
func saveLayerChange(store database.Datastore, layer *database.Layer) error {
if err := database.PersistFeaturesAndCommit(store, layer.GetFeatures()); err != nil {
return err
}
if err := database.PersistNamespacesAndCommit(store, layer.GetNamespaces()); err != nil {
return err
}
if err := database.PersistPartialLayerAndCommit(store, layer); err != nil {
return err
}
return nil
}

@ -0,0 +1,355 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clair
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"strings"
log "github.com/sirupsen/logrus"
"github.com/coreos/clair/database"
)
type layerIndexedFeature struct {
Feature *database.LayerFeature
Namespace *layerIndexedNamespace
IntroducedIn int
}
type layerIndexedNamespace struct {
Namespace database.LayerNamespace `json:"namespace"`
IntroducedIn int `json:"introducedIn"`
}
// AncestryBuilder builds an Ancestry, which contains an ordered list of layers
// and their features.
type AncestryBuilder struct {
layerIndex int
layerNames []string
detectors []database.Detector
namespaces []layerIndexedNamespace // unique namespaces
features map[database.Detector][]layerIndexedFeature
}
// NewAncestryBuilder creates a new ancestry builder.
//
// ancestry builder takes in the extracted layer information and produce a set of
// namespaces, features, and the relation between features for the whole image.
func NewAncestryBuilder(detectors []database.Detector) *AncestryBuilder {
return &AncestryBuilder{
layerIndex: 0,
detectors: detectors,
namespaces: make([]layerIndexedNamespace, 0),
features: make(map[database.Detector][]layerIndexedFeature),
}
}
// AddLeafLayer adds a leaf layer to the ancestry builder, and computes the
// namespaced features.
func (b *AncestryBuilder) AddLeafLayer(layer *database.Layer) {
b.layerNames = append(b.layerNames, layer.Hash)
for i := range layer.Namespaces {
b.updateNamespace(&layer.Namespaces[i])
}
allFeatureMap := map[database.Detector][]database.LayerFeature{}
for i := range layer.Features {
layerFeature := layer.Features[i]
allFeatureMap[layerFeature.By] = append(allFeatureMap[layerFeature.By], layerFeature)
}
// we only care about the ones specified by builder's detectors
featureMap := map[database.Detector][]database.LayerFeature{}
for i := range b.detectors {
detector := b.detectors[i]
featureMap[detector] = allFeatureMap[detector]
}
for detector := range featureMap {
b.addLayerFeatures(detector, featureMap[detector])
}
b.layerIndex++
}
// Every detector inspects a set of files for the features
// therefore, if that set of files gives a different set of features, it
// should replace the existing features.
func (b *AncestryBuilder) addLayerFeatures(detector database.Detector, features []database.LayerFeature) {
if len(features) == 0 {
// TODO(sidac): we need to differentiate if the detector finds that all
// features are removed ( a file change ), or the package installer is
// removed ( a file deletion ), or there's no change in the file ( file
// does not exist in the blob ) Right now, we're just assuming that no
// change in the file because that's the most common case.
return
}
existingFeatures := b.features[detector]
currentFeatures := make([]layerIndexedFeature, 0, len(features))
// Features that are not in the current layer should be removed.
for i := range existingFeatures {
feature := existingFeatures[i]
for j := range features {
if features[j] == *feature.Feature {
currentFeatures = append(currentFeatures, feature)
break
}
}
}
// Features that newly introduced in the current layer should be added.
for i := range features {
found := false
for j := range existingFeatures {
if *existingFeatures[j].Feature == features[i] {
found = true
break
}
}
if !found {
namespace, found := b.lookupNamespace(&features[i])
if !found {
continue
}
currentFeatures = append(currentFeatures, b.createLayerIndexedFeature(namespace, &features[i]))
}
}
b.features[detector] = currentFeatures
}
// updateNamespace update the namespaces for the ancestry. It does the following things:
// 1. when a detector detects a new namespace, it's added to the ancestry.
// 2. when a detector detects a difference in the detected namespace, it
// replaces the namespace, and also move all features under that namespace to
// the new namespace.
func (b *AncestryBuilder) updateNamespace(layerNamespace *database.LayerNamespace) {
var (
previous *layerIndexedNamespace
foundUpgrade bool
)
newNSNames := strings.Split(layerNamespace.Name, ":")
if len(newNSNames) != 2 {
log.Error("invalid namespace name")
}
newNSName := newNSNames[0]
newNSVersion := newNSNames[1]
for i, ns := range b.namespaces {
nsNames := strings.Split(ns.Namespace.Name, ":")
if len(nsNames) != 2 {
log.Error("invalid namespace name")
continue
}
nsName := nsNames[0]
nsVersion := nsNames[1]
if ns.Namespace.VersionFormat == layerNamespace.VersionFormat && nsName == newNSName {
if nsVersion != newNSVersion {
previous = &b.namespaces[i]
foundUpgrade = true
break
} else {
// not changed
return
}
}
}
// we didn't found the namespace is a upgrade from another namespace, so we
// simply add it.
if !foundUpgrade {
b.namespaces = append(b.namespaces, layerIndexedNamespace{
Namespace: *layerNamespace,
IntroducedIn: b.layerIndex,
})
return
}
// All features referencing to this namespace are now pointing to the new namespace.
// Also those features are now treated as introduced in the same layer as
// when this new namespace is introduced.
previous.Namespace = *layerNamespace
previous.IntroducedIn = b.layerIndex
for _, features := range b.features {
for i, feature := range features {
if feature.Namespace == previous {
features[i].IntroducedIn = previous.IntroducedIn
}
}
}
}
func (b *AncestryBuilder) createLayerIndexedFeature(namespace *layerIndexedNamespace, feature *database.LayerFeature) layerIndexedFeature {
return layerIndexedFeature{
Feature: feature,
Namespace: namespace,
IntroducedIn: b.layerIndex,
}
}
func (b *AncestryBuilder) lookupNamespace(feature *database.LayerFeature) (*layerIndexedNamespace, bool) {
matchedNamespaces := []*layerIndexedNamespace{}
if feature.PotentialNamespace.Name != "" {
a := &layerIndexedNamespace{
Namespace: database.LayerNamespace{
Namespace: feature.PotentialNamespace,
},
IntroducedIn: b.layerIndex,
}
matchedNamespaces = append(matchedNamespaces, a)
} else {
for i, namespace := range b.namespaces {
if namespace.Namespace.VersionFormat == feature.VersionFormat {
matchedNamespaces = append(matchedNamespaces, &b.namespaces[i])
}
}
}
if len(matchedNamespaces) == 1 {
return matchedNamespaces[0], true
}
serialized, _ := json.Marshal(matchedNamespaces)
fields := log.Fields{
"feature.Name": feature.Name,
"feature.VersionFormat": feature.VersionFormat,
"ancestryBuilder.namespaces": string(serialized),
}
if len(matchedNamespaces) > 1 {
log.WithFields(fields).Warn("skip features with ambiguous namespaces")
} else {
log.WithFields(fields).Warn("skip features with no matching namespace")
}
return nil, false
}
func (b *AncestryBuilder) ancestryFeatures(index int) []database.AncestryFeature {
ancestryFeatures := []database.AncestryFeature{}
for detector, features := range b.features {
for _, feature := range features {
if feature.IntroducedIn == index {
ancestryFeatures = append(ancestryFeatures, database.AncestryFeature{
NamespacedFeature: database.NamespacedFeature{
Feature: feature.Feature.Feature,
Namespace: feature.Namespace.Namespace.Namespace,
},
FeatureBy: detector,
NamespaceBy: feature.Namespace.Namespace.By,
})
}
}
}
return ancestryFeatures
}
func (b *AncestryBuilder) ancestryLayers() []database.AncestryLayer {
layers := make([]database.AncestryLayer, 0, b.layerIndex)
for i := 0; i < b.layerIndex; i++ {
layers = append(layers, database.AncestryLayer{
Hash: b.layerNames[i],
Features: b.ancestryFeatures(i),
})
}
return layers
}
// Ancestry produces an Ancestry from the builder.
func (b *AncestryBuilder) Ancestry(name string) *database.Ancestry {
if name == "" {
// TODO(sidac): we'll use the computed ancestry name in the future.
// During the transition, it still requires the user to use the correct
// ancestry name.
name = ancestryName(b.layerNames)
log.WithField("ancestry.Name", name).Warn("generated ancestry name since it's not specified")
}
return &database.Ancestry{
Name: name,
By: b.detectors,
Layers: b.ancestryLayers(),
}
}
// SaveAncestry saves an ancestry to the datastore.
func SaveAncestry(store database.Datastore, ancestry *database.Ancestry) error {
log.WithField("ancestry.Name", ancestry.Name).Debug("saving ancestry")
features := []database.NamespacedFeature{}
for _, layer := range ancestry.Layers {
features = append(features, layer.GetFeatures()...)
}
if err := database.PersistNamespacedFeaturesAndCommit(store, features); err != nil {
return StorageError
}
if err := database.UpsertAncestryAndCommit(store, ancestry); err != nil {
return StorageError
}
if err := database.CacheRelatedVulnerabilityAndCommit(store, features); err != nil {
return StorageError
}
return nil
}
// IsAncestryCached checks if the ancestry is already cached in the database with the current set of detectors.
func IsAncestryCached(store database.Datastore, name string, layerHashes []string) (bool, error) {
if name == "" {
// TODO(sidac): we'll use the computed ancestry name in the future.
// During the transition, it still requires the user to use the correct
// ancestry name.
name = ancestryName(layerHashes)
log.WithField("ancestry.Name", name).Warn("generated ancestry name since it's not specified")
}
ancestry, found, err := database.FindAncestryAndRollback(store, name)
if err != nil {
log.WithError(err).WithField("ancestry.Name", name).Error("failed to query ancestry in database")
return false, StorageError
}
if found {
if len(database.DiffDetectors(EnabledDetectors(), ancestry.By)) == 0 {
log.WithField("ancestry.Name", name).Debug("found cached ancestry")
} else {
log.WithField("ancestry.Name", name).Debug("found outdated ancestry cache")
}
} else {
log.WithField("ancestry.Name", name).Debug("ancestry not cached")
}
return found && len(database.DiffDetectors(EnabledDetectors(), ancestry.By)) == 0, nil
}
func ancestryName(layerHashes []string) string {
tag := sha256.Sum256([]byte(strings.Join(layerHashes, ",")))
return hex.EncodeToString(tag[:])
}

@ -0,0 +1,297 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clair
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coreos/clair/database"
)
var (
dpkg = database.NewFeatureDetector("dpkg", "1.0")
rpm = database.NewFeatureDetector("rpm", "1.0")
pip = database.NewFeatureDetector("pip", "1.0")
python = database.NewNamespaceDetector("python", "1.0")
osrelease = database.NewNamespaceDetector("os-release", "1.0")
aptsources = database.NewNamespaceDetector("apt-sources", "1.0")
ubuntu = *database.NewNamespace("ubuntu:14.04", "dpkg")
ubuntu16 = *database.NewNamespace("ubuntu:16.04", "dpkg")
rhel7 = *database.NewNamespace("cpe:/o:redhat:enterprise_linux:7::computenode", "rpm")
debian = *database.NewNamespace("debian:7", "dpkg")
python2 = *database.NewNamespace("python:2", "pip")
sed = *database.NewSourcePackage("sed", "4.4-2", "dpkg")
sedByRPM = *database.NewBinaryPackage("sed", "4.4-2", "rpm")
sedBin = *database.NewBinaryPackage("sed", "4.4-2", "dpkg")
tar = *database.NewBinaryPackage("tar", "1.29b-2", "dpkg")
scipy = *database.NewSourcePackage("scipy", "3.0.0", "pip")
emptyNamespace = database.Namespace{}
detectors = []database.Detector{dpkg, osrelease, rpm}
multinamespaceDetectors = []database.Detector{dpkg, osrelease, pip}
)
type ancestryBuilder struct {
ancestry *database.Ancestry
}
func newAncestryBuilder(name string) *ancestryBuilder {
return &ancestryBuilder{&database.Ancestry{Name: name}}
}
func (b *ancestryBuilder) addDetectors(d ...database.Detector) *ancestryBuilder {
b.ancestry.By = append(b.ancestry.By, d...)
return b
}
func (b *ancestryBuilder) addLayer(hash string, f ...database.AncestryFeature) *ancestryBuilder {
l := database.AncestryLayer{Hash: hash}
l.Features = append(l.Features, f...)
b.ancestry.Layers = append(b.ancestry.Layers, l)
return b
}
func ancestryFeature(namespace database.Namespace, feature database.Feature, nsBy database.Detector, fBy database.Detector) database.AncestryFeature {
return database.AncestryFeature{
NamespacedFeature: database.NamespacedFeature{feature, namespace},
FeatureBy: fBy,
NamespaceBy: nsBy,
}
}
// layerBuilder is for helping constructing the layer test artifacts.
type layerBuilder struct {
layer *database.Layer
}
func newLayerBuilder(hash string) *layerBuilder {
return &layerBuilder{&database.Layer{Hash: hash, By: detectors}}
}
func newLayerBuilderWithoutDetector(hash string) *layerBuilder {
return &layerBuilder{&database.Layer{Hash: hash}}
}
func (b *layerBuilder) addDetectors(d ...database.Detector) *layerBuilder {
b.layer.By = append(b.layer.By, d...)
return b
}
func (b *layerBuilder) addNamespace(detector database.Detector, ns database.Namespace) *layerBuilder {
b.layer.Namespaces = append(b.layer.Namespaces, database.LayerNamespace{
Namespace: ns,
By: detector,
})
return b
}
func (b *layerBuilder) addFeature(detector database.Detector, f database.Feature, ns database.Namespace) *layerBuilder {
b.layer.Features = append(b.layer.Features, database.LayerFeature{
Feature: f,
By: detector,
PotentialNamespace: ns,
})
return b
}
var testImage = []*database.Layer{
// empty layer
newLayerBuilder("0").layer,
// ubuntu namespace
newLayerBuilder("1").addNamespace(osrelease, ubuntu).layer,
// install sed
newLayerBuilder("2").addFeature(dpkg, sed, emptyNamespace).layer,
// install tar
newLayerBuilder("3").addFeature(dpkg, sed, emptyNamespace).addFeature(dpkg, tar, emptyNamespace).layer,
// remove tar
newLayerBuilder("4").addFeature(dpkg, sed, emptyNamespace).layer,
// upgrade ubuntu
newLayerBuilder("5").addNamespace(osrelease, ubuntu16).layer,
// no change to the detectable files
newLayerBuilder("6").layer,
// change to the package installer database but no features are affected.
newLayerBuilder("7").addFeature(dpkg, sed, emptyNamespace).layer,
}
var invalidNamespace = []*database.Layer{
// add package without namespace, this indicates that the namespace detector
// could not detect the namespace.
newLayerBuilder("0").addFeature(dpkg, sed, emptyNamespace).layer,
}
var noMatchingNamespace = []*database.Layer{
newLayerBuilder("0").addFeature(rpm, sedByRPM, emptyNamespace).addFeature(dpkg, sed, emptyNamespace).addNamespace(osrelease, ubuntu).layer,
}
var multiplePackagesOnFirstLayer = []*database.Layer{
newLayerBuilder("0").addFeature(dpkg, sed, emptyNamespace).addFeature(dpkg, tar, emptyNamespace).addFeature(dpkg, sedBin, emptyNamespace).addNamespace(osrelease, ubuntu16).layer,
}
var twoNamespaceDetectorsWithSameResult = []*database.Layer{
newLayerBuilderWithoutDetector("0").addDetectors(dpkg, aptsources, osrelease).addFeature(dpkg, sed, emptyNamespace).addNamespace(aptsources, ubuntu).addNamespace(osrelease, ubuntu).layer,
}
var sameVersionFormatDiffName = []*database.Layer{
newLayerBuilder("0").addFeature(dpkg, sed, emptyNamespace).addNamespace(aptsources, ubuntu).addNamespace(osrelease, debian).layer,
}
var potentialFeatureNamespace = []*database.Layer{
newLayerBuilder("0").addFeature(rpm, sed, rhel7).layer,
}
func TestAddLayer(t *testing.T) {
cases := []struct {
title string
image []*database.Layer
nonDefaultDetectors []database.Detector
expectedAncestry database.Ancestry
}{
{
title: "empty image",
expectedAncestry: *newAncestryBuilder(ancestryName([]string{})).addDetectors(detectors...).ancestry,
},
{
title: "empty layer",
image: testImage[:1],
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(detectors...).
addLayer("0").ancestry,
},
{
title: "ubuntu",
image: testImage[:2],
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1"})).addDetectors(detectors...).
addLayer("0").
addLayer("1").ancestry,
},
{
title: "ubuntu install sed",
image: testImage[:3],
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1", "2"})).addDetectors(detectors...).
addLayer("0").
addLayer("1").
addLayer("2", ancestryFeature(ubuntu, sed, osrelease, dpkg)).ancestry,
},
{
title: "ubuntu install tar",
image: testImage[:4],
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1", "2", "3"})).addDetectors(detectors...).
addLayer("0").
addLayer("1").
addLayer("2", ancestryFeature(ubuntu, sed, osrelease, dpkg)).
addLayer("3", ancestryFeature(ubuntu, tar, osrelease, dpkg)).ancestry,
}, {
title: "ubuntu uninstall tar",
image: testImage[:5],
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1", "2", "3", "4"})).addDetectors(detectors...).
addLayer("0").
addLayer("1").
addLayer("2", ancestryFeature(ubuntu, sed, osrelease, dpkg)).
addLayer("3").
addLayer("4").ancestry,
}, {
title: "ubuntu upgrade",
image: testImage[:6],
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1", "2", "3", "4", "5"})).addDetectors(detectors...).
addLayer("0").
addLayer("1").
addLayer("2").
addLayer("3").
addLayer("4").
addLayer("5", ancestryFeature(ubuntu16, sed, osrelease, dpkg)).ancestry,
}, {
title: "no change to the detectable files",
image: testImage[:7],
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1", "2", "3", "4", "5", "6"})).addDetectors(detectors...).
addLayer("0").
addLayer("1").
addLayer("2").
addLayer("3").
addLayer("4").
addLayer("5", ancestryFeature(ubuntu16, sed, osrelease, dpkg)).
addLayer("6").ancestry,
}, {
title: "change to the package installer database but no features are affected.",
image: testImage[:8],
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0", "1", "2", "3", "4", "5", "6", "7"})).addDetectors(detectors...).
addLayer("0").
addLayer("1").
addLayer("2").
addLayer("3").
addLayer("4").
addLayer("5", ancestryFeature(ubuntu16, sed, osrelease, dpkg)).
addLayer("6").
addLayer("7").ancestry,
}, {
title: "layers with features and namespace.",
image: multiplePackagesOnFirstLayer,
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(detectors...).
addLayer("0",
ancestryFeature(ubuntu16, sed, osrelease, dpkg),
ancestryFeature(ubuntu16, sedBin, osrelease, dpkg),
ancestryFeature(ubuntu16, tar, osrelease, dpkg)).
ancestry,
}, {
title: "two namespace detectors giving same namespace.",
image: twoNamespaceDetectorsWithSameResult,
nonDefaultDetectors: []database.Detector{osrelease, aptsources, dpkg},
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(osrelease, aptsources, dpkg).
addLayer("0", ancestryFeature(ubuntu, sed, aptsources, dpkg)).
ancestry,
}, {
title: "feature without namespace",
image: invalidNamespace,
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(detectors...).
addLayer("0").
ancestry,
}, {
title: "two namespaces with the same version format but different names",
image: sameVersionFormatDiffName,
// failure of matching a namespace will result in the package not being added.
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(detectors...).
addLayer("0").
ancestry,
}, {
title: "noMatchingNamespace",
image: noMatchingNamespace,
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(detectors...).addLayer("0", ancestryFeature(ubuntu, sed, osrelease, dpkg)).ancestry,
}, {
title: "featureWithPotentialNamespace",
image: potentialFeatureNamespace,
expectedAncestry: *newAncestryBuilder(ancestryName([]string{"0"})).addDetectors(detectors...).addLayer("0", ancestryFeature(rhel7, sed, database.Detector{}, rpm)).ancestry,
},
}
for _, test := range cases {
t.Run(test.title, func(t *testing.T) {
var builder *AncestryBuilder
if len(test.nonDefaultDetectors) != 0 {
builder = NewAncestryBuilder(test.nonDefaultDetectors)
} else {
builder = NewAncestryBuilder(detectors)
}
for _, layer := range test.image {
builder.AddLeafLayer(layer)
}
ancestry := builder.Ancestry("")
require.True(t, database.AssertAncestryEqual(t, &test.expectedAncestry, ancestry))
})
}
}

@ -1,4 +1,4 @@
// Copyright 2017 clair authors
// Copyright 2018 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -15,17 +15,13 @@
package api
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"net"
"context"
"net/http"
"strconv"
"time"
log "github.com/sirupsen/logrus"
"github.com/tylerb/graceful"
"github.com/coreos/clair/api/v3"
"github.com/coreos/clair/database"
"github.com/coreos/clair/pkg/stopper"
)
@ -34,44 +30,17 @@ const timeoutResponse = `{"Error":{"Message":"Clair failed to respond within the
// Config is the configuration for the API service.
type Config struct {
Port int
HealthPort int
Addr string
HealthAddr string
Timeout time.Duration
PaginationKey string
CertFile, KeyFile, CAFile string
}
func Run(cfg *Config, store database.Datastore, st *stopper.Stopper) {
defer st.End()
// Do not run the API service if there is no config.
if cfg == nil {
log.Info("main API service is disabled.")
return
}
log.WithField("port", cfg.Port).Info("starting main API")
tlsConfig, err := tlsClientConfig(cfg.CAFile)
func Run(cfg *Config, store database.Datastore) {
err := v3.ListenAndServe(cfg.Addr, cfg.CertFile, cfg.KeyFile, cfg.CAFile, store)
if err != nil {
log.WithError(err).Fatal("could not initialize client cert authentication")
}
if tlsConfig != nil {
log.Info("main API configured with client certificate authentication")
log.WithError(err).Fatal("could not initialize gRPC server")
}
srv := &graceful.Server{
Timeout: 0, // Already handled by our TimeOut middleware
NoSignalHandling: true, // We want to use our own Stopper
Server: &http.Server{
Addr: ":" + strconv.Itoa(cfg.Port),
TLSConfig: tlsConfig,
Handler: http.TimeoutHandler(newAPIHandler(cfg, store), cfg.Timeout, timeoutResponse),
},
}
listenAndServeWithStopper(srv, st, cfg.CertFile, cfg.KeyFile)
log.Info("main API stopped")
}
func RunHealth(cfg *Config, store database.Datastore, st *stopper.Stopper) {
@ -82,69 +51,22 @@ func RunHealth(cfg *Config, store database.Datastore, st *stopper.Stopper) {
log.Info("health API service is disabled.")
return
}
log.WithField("port", cfg.HealthPort).Info("starting health API")
log.WithField("addr", cfg.HealthAddr).Info("starting health API")
srv := &graceful.Server{
Timeout: 10 * time.Second, // Interrupt health checks when stopping
NoSignalHandling: true, // We want to use our own Stopper
Server: &http.Server{
Addr: ":" + strconv.Itoa(cfg.HealthPort),
Handler: http.TimeoutHandler(newHealthHandler(store), cfg.Timeout, timeoutResponse),
},
srv := http.Server{
Addr: cfg.HealthAddr,
Handler: http.TimeoutHandler(newHealthHandler(store), cfg.Timeout, timeoutResponse),
}
listenAndServeWithStopper(srv, st, "", "")
log.Info("health API stopped")
}
// listenAndServeWithStopper wraps graceful.Server's
// ListenAndServe/ListenAndServeTLS and adds the ability to interrupt them with
// the provided stopper.Stopper.
func listenAndServeWithStopper(srv *graceful.Server, st *stopper.Stopper, certFile, keyFile string) {
go func() {
<-st.Chan()
srv.Stop(0)
srv.Shutdown(context.TODO())
}()
var err error
if certFile != "" && keyFile != "" {
log.Info("API: TLS Enabled")
err = srv.ListenAndServeTLS(certFile, keyFile)
} else {
err = srv.ListenAndServe()
}
if err != nil {
if opErr, ok := err.(*net.OpError); !ok || (ok && opErr.Op != "accept") {
log.Fatal(err)
}
}
}
// tlsClientConfig initializes a *tls.Config using the given CA. The resulting
// *tls.Config is meant to be used to configure an HTTP server to do client
// certificate authentication.
//
// If no CA is given, a nil *tls.Config is returned; no client certificate will
// be required and verified. In other words, authentication will be disabled.
func tlsClientConfig(caPath string) (*tls.Config, error) {
if caPath == "" {
return nil, nil
err := srv.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
caCert, err := ioutil.ReadFile(caPath)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
}
return tlsConfig, nil
log.Info("health API stopped")
}

@ -16,12 +16,9 @@ package api
import (
"net/http"
"strings"
"github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus"
"github.com/coreos/clair/api/v1"
"github.com/coreos/clair/database"
)
@ -29,34 +26,6 @@ import (
// depending on the API version specified in the request URI.
type router map[string]*httprouter.Router
// Let's hope we never have more than 99 API versions.
const apiVersionLength = len("v99")
func newAPIHandler(cfg *Config, store database.Datastore) http.Handler {
router := make(router)
router["/v1"] = v1.NewRouter(store, cfg.PaginationKey)
return router
}
func (rtr router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
urlStr := r.URL.String()
var version string
if len(urlStr) >= apiVersionLength {
version = urlStr[:apiVersionLength]
}
if router, _ := rtr[version]; router != nil {
// Remove the version number from the request path to let the router do its
// job but do not update the RequestURI
r.URL.Path = strings.Replace(r.URL.Path, version, "", 1)
router.ServeHTTP(w, r)
return
}
log.WithFields(log.Fields{"status": http.StatusNotFound, "method": r.Method, "request uri": r.RequestURI, "remote addr": r.RemoteAddr}).Info("Served HTTP request")
http.NotFound(w, r)
}
func newHealthHandler(store database.Datastore) http.Handler {
router := httprouter.New()
router.GET("/health", healthHandler(store))

@ -1,343 +0,0 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/fernet/fernet-go"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
)
type Error struct {
Message string `json:"Message,omitempty"`
}
type Layer struct {
Name string `json:"Name,omitempty"`
NamespaceName string `json:"NamespaceName,omitempty"`
Path string `json:"Path,omitempty"`
Headers map[string]string `json:"Headers,omitempty"`
ParentName string `json:"ParentName,omitempty"`
Format string `json:"Format,omitempty"`
IndexedByVersion int `json:"IndexedByVersion,omitempty"`
Features []Feature `json:"Features,omitempty"`
}
func LayerFromDatabaseModel(dbLayer database.Layer, withFeatures, withVulnerabilities bool) Layer {
layer := Layer{
Name: dbLayer.Name,
IndexedByVersion: dbLayer.EngineVersion,
}
if dbLayer.Parent != nil {
layer.ParentName = dbLayer.Parent.Name
}
if dbLayer.Namespace != nil {
layer.NamespaceName = dbLayer.Namespace.Name
}
if withFeatures || withVulnerabilities && dbLayer.Features != nil {
for _, dbFeatureVersion := range dbLayer.Features {
feature := Feature{
Name: dbFeatureVersion.Feature.Name,
NamespaceName: dbFeatureVersion.Feature.Namespace.Name,
VersionFormat: dbFeatureVersion.Feature.Namespace.VersionFormat,
Version: dbFeatureVersion.Version,
AddedBy: dbFeatureVersion.AddedBy.Name,
}
for _, dbVuln := range dbFeatureVersion.AffectedBy {
vuln := Vulnerability{
Name: dbVuln.Name,
NamespaceName: dbVuln.Namespace.Name,
Description: dbVuln.Description,
Link: dbVuln.Link,
Severity: string(dbVuln.Severity),
Metadata: dbVuln.Metadata,
}
if dbVuln.FixedBy != versionfmt.MaxVersion {
vuln.FixedBy = dbVuln.FixedBy
}
feature.Vulnerabilities = append(feature.Vulnerabilities, vuln)
}
layer.Features = append(layer.Features, feature)
}
}
return layer
}
type Namespace struct {
Name string `json:"Name,omitempty"`
VersionFormat string `json:"VersionFormat,omitempty"`
}
type Vulnerability struct {
Name string `json:"Name,omitempty"`
NamespaceName string `json:"NamespaceName,omitempty"`
Description string `json:"Description,omitempty"`
Link string `json:"Link,omitempty"`
Severity string `json:"Severity,omitempty"`
Metadata map[string]interface{} `json:"Metadata,omitempty"`
FixedBy string `json:"FixedBy,omitempty"`
FixedIn []Feature `json:"FixedIn,omitempty"`
}
func (v Vulnerability) DatabaseModel() (database.Vulnerability, error) {
severity, err := database.NewSeverity(v.Severity)
if err != nil {
return database.Vulnerability{}, err
}
var dbFeatures []database.FeatureVersion
for _, feature := range v.FixedIn {
dbFeature, err := feature.DatabaseModel()
if err != nil {
return database.Vulnerability{}, err
}
dbFeatures = append(dbFeatures, dbFeature)
}
return database.Vulnerability{
Name: v.Name,
Namespace: database.Namespace{Name: v.NamespaceName},
Description: v.Description,
Link: v.Link,
Severity: severity,
Metadata: v.Metadata,
FixedIn: dbFeatures,
}, nil
}
func VulnerabilityFromDatabaseModel(dbVuln database.Vulnerability, withFixedIn bool) Vulnerability {
vuln := Vulnerability{
Name: dbVuln.Name,
NamespaceName: dbVuln.Namespace.Name,
Description: dbVuln.Description,
Link: dbVuln.Link,
Severity: string(dbVuln.Severity),
Metadata: dbVuln.Metadata,
}
if withFixedIn {
for _, dbFeatureVersion := range dbVuln.FixedIn {
vuln.FixedIn = append(vuln.FixedIn, FeatureFromDatabaseModel(dbFeatureVersion))
}
}
return vuln
}
type Feature struct {
Name string `json:"Name,omitempty"`
NamespaceName string `json:"NamespaceName,omitempty"`
VersionFormat string `json:"VersionFormat,omitempty"`
Version string `json:"Version,omitempty"`
Vulnerabilities []Vulnerability `json:"Vulnerabilities,omitempty"`
AddedBy string `json:"AddedBy,omitempty"`
}
func FeatureFromDatabaseModel(dbFeatureVersion database.FeatureVersion) Feature {
version := dbFeatureVersion.Version
if version == versionfmt.MaxVersion {
version = "None"
}
return Feature{
Name: dbFeatureVersion.Feature.Name,
NamespaceName: dbFeatureVersion.Feature.Namespace.Name,
VersionFormat: dbFeatureVersion.Feature.Namespace.VersionFormat,
Version: version,
AddedBy: dbFeatureVersion.AddedBy.Name,
}
}
func (f Feature) DatabaseModel() (fv database.FeatureVersion, err error) {
var version string
if f.Version == "None" {
version = versionfmt.MaxVersion
} else {
err = versionfmt.Valid(f.VersionFormat, f.Version)
if err != nil {
return
}
version = f.Version
}
fv = database.FeatureVersion{
Feature: database.Feature{
Name: f.Name,
Namespace: database.Namespace{
Name: f.NamespaceName,
VersionFormat: f.VersionFormat,
},
},
Version: version,
}
return
}
type Notification struct {
Name string `json:"Name,omitempty"`
Created string `json:"Created,omitempty"`
Notified string `json:"Notified,omitempty"`
Deleted string `json:"Deleted,omitempty"`
Limit int `json:"Limit,omitempty"`
Page string `json:"Page,omitempty"`
NextPage string `json:"NextPage,omitempty"`
Old *VulnerabilityWithLayers `json:"Old,omitempty"`
New *VulnerabilityWithLayers `json:"New,omitempty"`
}
func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotification, limit int, pageToken string, nextPage database.VulnerabilityNotificationPageNumber, key string) Notification {
var oldVuln *VulnerabilityWithLayers
if dbNotification.OldVulnerability != nil {
v := VulnerabilityWithLayersFromDatabaseModel(*dbNotification.OldVulnerability)
oldVuln = &v
}
var newVuln *VulnerabilityWithLayers
if dbNotification.NewVulnerability != nil {
v := VulnerabilityWithLayersFromDatabaseModel(*dbNotification.NewVulnerability)
newVuln = &v
}
var nextPageStr string
if nextPage != database.NoVulnerabilityNotificationPage {
nextPageBytes, _ := tokenMarshal(nextPage, key)
nextPageStr = string(nextPageBytes)
}
var created, notified, deleted string
if !dbNotification.Created.IsZero() {
created = fmt.Sprintf("%d", dbNotification.Created.Unix())
}
if !dbNotification.Notified.IsZero() {
notified = fmt.Sprintf("%d", dbNotification.Notified.Unix())
}
if !dbNotification.Deleted.IsZero() {
deleted = fmt.Sprintf("%d", dbNotification.Deleted.Unix())
}
// TODO(jzelinskie): implement "changed" key
fmt.Println(dbNotification.Deleted.IsZero())
return Notification{
Name: dbNotification.Name,
Created: created,
Notified: notified,
Deleted: deleted,
Limit: limit,
Page: pageToken,
NextPage: nextPageStr,
Old: oldVuln,
New: newVuln,
}
}
type VulnerabilityWithLayers struct {
Vulnerability *Vulnerability `json:"Vulnerability,omitempty"`
// This field is guaranteed to be in order only for pagination.
// Indices from different notifications may not be comparable.
OrderedLayersIntroducingVulnerability []OrderedLayerName `json:"OrderedLayersIntroducingVulnerability,omitempty"`
// This field is deprecated.
LayersIntroducingVulnerability []string `json:"LayersIntroducingVulnerability,omitempty"`
}
type OrderedLayerName struct {
Index int `json:"Index"`
LayerName string `json:"LayerName"`
}
func VulnerabilityWithLayersFromDatabaseModel(dbVuln database.Vulnerability) VulnerabilityWithLayers {
vuln := VulnerabilityFromDatabaseModel(dbVuln, true)
var layers []string
var orderedLayers []OrderedLayerName
for _, layer := range dbVuln.LayersIntroducingVulnerability {
layers = append(layers, layer.Name)
orderedLayers = append(orderedLayers, OrderedLayerName{
Index: layer.ID,
LayerName: layer.Name,
})
}
return VulnerabilityWithLayers{
Vulnerability: &vuln,
OrderedLayersIntroducingVulnerability: orderedLayers,
LayersIntroducingVulnerability: layers,
}
}
type LayerEnvelope struct {
Layer *Layer `json:"Layer,omitempty"`
Error *Error `json:"Error,omitempty"`
}
type NamespaceEnvelope struct {
Namespaces *[]Namespace `json:"Namespaces,omitempty"`
Error *Error `json:"Error,omitempty"`
}
type VulnerabilityEnvelope struct {
Vulnerability *Vulnerability `json:"Vulnerability,omitempty"`
Vulnerabilities *[]Vulnerability `json:"Vulnerabilities,omitempty"`
NextPage string `json:"NextPage,omitempty"`
Error *Error `json:"Error,omitempty"`
}
type NotificationEnvelope struct {
Notification *Notification `json:"Notification,omitempty"`
Error *Error `json:"Error,omitempty"`
}
type FeatureEnvelope struct {
Feature *Feature `json:"Feature,omitempty"`
Features *[]Feature `json:"Features,omitempty"`
Error *Error `json:"Error,omitempty"`
}
func tokenUnmarshal(token string, key string, v interface{}) error {
k, _ := fernet.DecodeKey(key)
msg := fernet.VerifyAndDecrypt([]byte(token), time.Hour, []*fernet.Key{k})
if msg == nil {
return errors.New("invalid or expired pagination token")
}
return json.NewDecoder(bytes.NewBuffer(msg)).Decode(&v)
}
func tokenMarshal(v interface{}, key string) ([]byte, error) {
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(v)
if err != nil {
return nil, err
}
k, _ := fernet.DecodeKey(key)
return fernet.EncryptAndSign(buf.Bytes(), k)
}

@ -1,99 +0,0 @@
// Copyright 2015 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package v1 implements the first version of the Clair API.
package v1
import (
"net/http"
"strconv"
"time"
"github.com/julienschmidt/httprouter"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"github.com/coreos/clair/database"
)
var (
promResponseDurationMilliseconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "clair_api_response_duration_milliseconds",
Help: "The duration of time it takes to receieve and write a response to an API request",
Buckets: prometheus.ExponentialBuckets(9.375, 2, 10),
}, []string{"route", "code"})
)
func init() {
prometheus.MustRegister(promResponseDurationMilliseconds)
}
type handler func(http.ResponseWriter, *http.Request, httprouter.Params, *context) (route string, status int)
func httpHandler(h handler, ctx *context) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
start := time.Now()
route, status := h(w, r, p, ctx)
statusStr := strconv.Itoa(status)
if status == 0 {
statusStr = "???"
}
promResponseDurationMilliseconds.
WithLabelValues(route, statusStr).
Observe(float64(time.Since(start).Nanoseconds()) / float64(time.Millisecond))
log.WithFields(log.Fields{"remote addr": r.RemoteAddr, "method": r.Method, "request uri": r.RequestURI, "status": statusStr, "elapsed time": time.Since(start)}).Info("Handled HTTP request")
}
}
type context struct {
Store database.Datastore
PaginationKey string
}
// NewRouter creates an HTTP router for version 1 of the Clair API.
func NewRouter(store database.Datastore, paginationKey string) *httprouter.Router {
router := httprouter.New()
ctx := &context{store, paginationKey}
// Layers
router.POST("/layers", httpHandler(postLayer, ctx))
router.GET("/layers/:layerName", httpHandler(getLayer, ctx))
router.DELETE("/layers/:layerName", httpHandler(deleteLayer, ctx))
// Namespaces
router.GET("/namespaces", httpHandler(getNamespaces, ctx))
// Vulnerabilities
router.GET("/namespaces/:namespaceName/vulnerabilities", httpHandler(getVulnerabilities, ctx))
router.POST("/namespaces/:namespaceName/vulnerabilities", httpHandler(postVulnerability, ctx))
router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", httpHandler(getVulnerability, ctx))
router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", httpHandler(putVulnerability, ctx))
router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", httpHandler(deleteVulnerability, ctx))
// Fixes
router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes", httpHandler(getFixes, ctx))
router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", httpHandler(putFix, ctx))
router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", httpHandler(deleteFix, ctx))
// Notifications
router.GET("/notifications/:notificationName", httpHandler(getNotification, ctx))
router.DELETE("/notifications/:notificationName", httpHandler(deleteNotification, ctx))
// Metrics
router.GET("/metrics", httpHandler(getMetrics, ctx))
return router
}

@ -1,502 +0,0 @@
// Copyright 2015 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1
import (
"compress/gzip"
"encoding/json"
"io"
"net/http"
"strconv"
"strings"
"github.com/julienschmidt/httprouter"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"github.com/coreos/clair"
"github.com/coreos/clair/database"
"github.com/coreos/clair/pkg/commonerr"
"github.com/coreos/clair/pkg/tarutil"
)
const (
// These are the route identifiers for prometheus.
postLayerRoute = "v1/postLayer"
getLayerRoute = "v1/getLayer"
deleteLayerRoute = "v1/deleteLayer"
getNamespacesRoute = "v1/getNamespaces"
getVulnerabilitiesRoute = "v1/getVulnerabilities"
postVulnerabilityRoute = "v1/postVulnerability"
getVulnerabilityRoute = "v1/getVulnerability"
putVulnerabilityRoute = "v1/putVulnerability"
deleteVulnerabilityRoute = "v1/deleteVulnerability"
getFixesRoute = "v1/getFixes"
putFixRoute = "v1/putFix"
deleteFixRoute = "v1/deleteFix"
getNotificationRoute = "v1/getNotification"
deleteNotificationRoute = "v1/deleteNotification"
getMetricsRoute = "v1/getMetrics"
// maxBodySize restricts client request bodies to 1MiB.
maxBodySize int64 = 1048576
// statusUnprocessableEntity represents the 422 (Unprocessable Entity) status code, which means
// the server understands the content type of the request entity
// (hence a 415(Unsupported Media Type) status code is inappropriate), and the syntax of the
// request entity is correct (thus a 400 (Bad Request) status code is inappropriate) but was
// unable to process the contained instructions.
statusUnprocessableEntity = 422
)
func decodeJSON(r *http.Request, v interface{}) error {
defer r.Body.Close()
return json.NewDecoder(io.LimitReader(r.Body, maxBodySize)).Decode(v)
}
func writeResponse(w http.ResponseWriter, r *http.Request, status int, resp interface{}) {
// Headers must be written before the response.
header := w.Header()
header.Set("Content-Type", "application/json;charset=utf-8")
header.Set("Server", "clair")
// Gzip the response if the client supports it.
var writer io.Writer = w
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
gzipWriter := gzip.NewWriter(w)
defer gzipWriter.Close()
writer = gzipWriter
header.Set("Content-Encoding", "gzip")
}
// Write the response.
w.WriteHeader(status)
err := json.NewEncoder(writer).Encode(resp)
if err != nil {
switch err.(type) {
case *json.MarshalerError, *json.UnsupportedTypeError, *json.UnsupportedValueError:
panic("v1: failed to marshal response: " + err.Error())
default:
log.WithError(err).Warning("failed to write response")
}
}
}
func postLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
request := LayerEnvelope{}
err := decodeJSON(r, &request)
if err != nil {
writeResponse(w, r, http.StatusBadRequest, LayerEnvelope{Error: &Error{err.Error()}})
return postLayerRoute, http.StatusBadRequest
}
if request.Layer == nil {
writeResponse(w, r, http.StatusBadRequest, LayerEnvelope{Error: &Error{"failed to provide layer"}})
return postLayerRoute, http.StatusBadRequest
}
err = clair.ProcessLayer(ctx.Store, request.Layer.Format, request.Layer.Name, request.Layer.ParentName, request.Layer.Path, request.Layer.Headers)
if err != nil {
if err == tarutil.ErrCouldNotExtract ||
err == tarutil.ErrExtractedFileTooBig ||
err == clair.ErrUnsupported {
writeResponse(w, r, statusUnprocessableEntity, LayerEnvelope{Error: &Error{err.Error()}})
return postLayerRoute, statusUnprocessableEntity
}
if _, badreq := err.(*commonerr.ErrBadRequest); badreq {
writeResponse(w, r, http.StatusBadRequest, LayerEnvelope{Error: &Error{err.Error()}})
return postLayerRoute, http.StatusBadRequest
}
writeResponse(w, r, http.StatusInternalServerError, LayerEnvelope{Error: &Error{err.Error()}})
return postLayerRoute, http.StatusInternalServerError
}
writeResponse(w, r, http.StatusCreated, LayerEnvelope{Layer: &Layer{
Name: request.Layer.Name,
ParentName: request.Layer.ParentName,
Path: request.Layer.Path,
Headers: request.Layer.Headers,
Format: request.Layer.Format,
IndexedByVersion: clair.Version,
}})
return postLayerRoute, http.StatusCreated
}
func getLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
_, withFeatures := r.URL.Query()["features"]
_, withVulnerabilities := r.URL.Query()["vulnerabilities"]
dbLayer, err := ctx.Store.FindLayer(p.ByName("layerName"), withFeatures, withVulnerabilities)
if err == commonerr.ErrNotFound {
writeResponse(w, r, http.StatusNotFound, LayerEnvelope{Error: &Error{err.Error()}})
return getLayerRoute, http.StatusNotFound
} else if err != nil {
writeResponse(w, r, http.StatusInternalServerError, LayerEnvelope{Error: &Error{err.Error()}})
return getLayerRoute, http.StatusInternalServerError
}
layer := LayerFromDatabaseModel(dbLayer, withFeatures, withVulnerabilities)
writeResponse(w, r, http.StatusOK, LayerEnvelope{Layer: &layer})
return getLayerRoute, http.StatusOK
}
func deleteLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
err := ctx.Store.DeleteLayer(p.ByName("layerName"))
if err == commonerr.ErrNotFound {
writeResponse(w, r, http.StatusNotFound, LayerEnvelope{Error: &Error{err.Error()}})
return deleteLayerRoute, http.StatusNotFound
} else if err != nil {
writeResponse(w, r, http.StatusInternalServerError, LayerEnvelope{Error: &Error{err.Error()}})
return deleteLayerRoute, http.StatusInternalServerError
}
w.WriteHeader(http.StatusOK)
return deleteLayerRoute, http.StatusOK
}
func getNamespaces(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
dbNamespaces, err := ctx.Store.ListNamespaces()
if err != nil {
writeResponse(w, r, http.StatusInternalServerError, NamespaceEnvelope{Error: &Error{err.Error()}})
return getNamespacesRoute, http.StatusInternalServerError
}
var namespaces []Namespace
for _, dbNamespace := range dbNamespaces {
namespaces = append(namespaces, Namespace{
Name: dbNamespace.Name,
VersionFormat: dbNamespace.VersionFormat,
})
}
writeResponse(w, r, http.StatusOK, NamespaceEnvelope{Namespaces: &namespaces})
return getNamespacesRoute, http.StatusOK
}
func getVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
query := r.URL.Query()
limitStrs, limitExists := query["limit"]
if !limitExists {
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"must provide limit query parameter"}})
return getVulnerabilitiesRoute, http.StatusBadRequest
}
limit, err := strconv.Atoi(limitStrs[0])
if err != nil {
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid limit format: " + err.Error()}})
return getVulnerabilitiesRoute, http.StatusBadRequest
} else if limit < 0 {
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"limit value should not be less than zero"}})
return getVulnerabilitiesRoute, http.StatusBadRequest
}
page := 0
pageStrs, pageExists := query["page"]
if pageExists {
err = tokenUnmarshal(pageStrs[0], ctx.PaginationKey, &page)
if err != nil {
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid page format: " + err.Error()}})
return getNotificationRoute, http.StatusBadRequest
}
}
namespace := p.ByName("namespaceName")
if namespace == "" {
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"namespace should not be empty"}})
return getNotificationRoute, http.StatusBadRequest
}
dbVulns, nextPage, err := ctx.Store.ListVulnerabilities(namespace, limit, page)
if err == commonerr.ErrNotFound {
writeResponse(w, r, http.StatusNotFound, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return getVulnerabilityRoute, http.StatusNotFound
} else if err != nil {
writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return getVulnerabilitiesRoute, http.StatusInternalServerError
}
var vulns []Vulnerability
for _, dbVuln := range dbVulns {
vuln := VulnerabilityFromDatabaseModel(dbVuln, false)
vulns = append(vulns, vuln)
}
var nextPageStr string
if nextPage != -1 {
nextPageBytes, err := tokenMarshal(nextPage, ctx.PaginationKey)
if err != nil {
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"failed to marshal token: " + err.Error()}})
return getNotificationRoute, http.StatusBadRequest
}
nextPageStr = string(nextPageBytes)
}
writeResponse(w, r, http.StatusOK, VulnerabilityEnvelope{Vulnerabilities: &vulns, NextPage: nextPageStr})
return getVulnerabilitiesRoute, http.StatusOK
}
func postVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
request := VulnerabilityEnvelope{}
err := decodeJSON(r, &request)
if err != nil {
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return postVulnerabilityRoute, http.StatusBadRequest
}
if request.Vulnerability == nil {
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"failed to provide vulnerability"}})
return postVulnerabilityRoute, http.StatusBadRequest
}
vuln, err := request.Vulnerability.DatabaseModel()
if err != nil {
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return postVulnerabilityRoute, http.StatusBadRequest
}
err = ctx.Store.InsertVulnerabilities([]database.Vulnerability{vuln}, true)
if err != nil {
switch err.(type) {
case *commonerr.ErrBadRequest:
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return postVulnerabilityRoute, http.StatusBadRequest
default:
writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return postVulnerabilityRoute, http.StatusInternalServerError
}
}
writeResponse(w, r, http.StatusCreated, VulnerabilityEnvelope{Vulnerability: request.Vulnerability})
return postVulnerabilityRoute, http.StatusCreated
}
func getVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
_, withFixedIn := r.URL.Query()["fixedIn"]
dbVuln, err := ctx.Store.FindVulnerability(p.ByName("namespaceName"), p.ByName("vulnerabilityName"))
if err == commonerr.ErrNotFound {
writeResponse(w, r, http.StatusNotFound, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return getVulnerabilityRoute, http.StatusNotFound
} else if err != nil {
writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return getVulnerabilityRoute, http.StatusInternalServerError
}
vuln := VulnerabilityFromDatabaseModel(dbVuln, withFixedIn)
writeResponse(w, r, http.StatusOK, VulnerabilityEnvelope{Vulnerability: &vuln})
return getVulnerabilityRoute, http.StatusOK
}
func putVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
request := VulnerabilityEnvelope{}
err := decodeJSON(r, &request)
if err != nil {
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return putVulnerabilityRoute, http.StatusBadRequest
}
if request.Vulnerability == nil {
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"failed to provide vulnerability"}})
return putVulnerabilityRoute, http.StatusBadRequest
}
if len(request.Vulnerability.FixedIn) != 0 {
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"Vulnerability.FixedIn must be empty"}})
return putVulnerabilityRoute, http.StatusBadRequest
}
vuln, err := request.Vulnerability.DatabaseModel()
if err != nil {
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return putVulnerabilityRoute, http.StatusBadRequest
}
vuln.Namespace.Name = p.ByName("namespaceName")
vuln.Name = p.ByName("vulnerabilityName")
err = ctx.Store.InsertVulnerabilities([]database.Vulnerability{vuln}, true)
if err != nil {
switch err.(type) {
case *commonerr.ErrBadRequest:
writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return putVulnerabilityRoute, http.StatusBadRequest
default:
writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return putVulnerabilityRoute, http.StatusInternalServerError
}
}
writeResponse(w, r, http.StatusOK, VulnerabilityEnvelope{Vulnerability: request.Vulnerability})
return putVulnerabilityRoute, http.StatusOK
}
func deleteVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
err := ctx.Store.DeleteVulnerability(p.ByName("namespaceName"), p.ByName("vulnerabilityName"))
if err == commonerr.ErrNotFound {
writeResponse(w, r, http.StatusNotFound, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return deleteVulnerabilityRoute, http.StatusNotFound
} else if err != nil {
writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}})
return deleteVulnerabilityRoute, http.StatusInternalServerError
}
w.WriteHeader(http.StatusOK)
return deleteVulnerabilityRoute, http.StatusOK
}
func getFixes(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
dbVuln, err := ctx.Store.FindVulnerability(p.ByName("namespaceName"), p.ByName("vulnerabilityName"))
if err == commonerr.ErrNotFound {
writeResponse(w, r, http.StatusNotFound, FeatureEnvelope{Error: &Error{err.Error()}})
return getFixesRoute, http.StatusNotFound
} else if err != nil {
writeResponse(w, r, http.StatusInternalServerError, FeatureEnvelope{Error: &Error{err.Error()}})
return getFixesRoute, http.StatusInternalServerError
}
vuln := VulnerabilityFromDatabaseModel(dbVuln, true)
writeResponse(w, r, http.StatusOK, FeatureEnvelope{Features: &vuln.FixedIn})
return getFixesRoute, http.StatusOK
}
func putFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
request := FeatureEnvelope{}
err := decodeJSON(r, &request)
if err != nil {
writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{err.Error()}})
return putFixRoute, http.StatusBadRequest
}
if request.Feature == nil {
writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{"failed to provide feature"}})
return putFixRoute, http.StatusBadRequest
}
if request.Feature.Name != p.ByName("fixName") {
writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{"feature name in URL and JSON do not match"}})
return putFixRoute, http.StatusBadRequest
}
dbFix, err := request.Feature.DatabaseModel()
if err != nil {
writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{err.Error()}})
return putFixRoute, http.StatusBadRequest
}
err = ctx.Store.InsertVulnerabilityFixes(p.ByName("vulnerabilityNamespace"), p.ByName("vulnerabilityName"), []database.FeatureVersion{dbFix})
if err != nil {
switch err.(type) {
case *commonerr.ErrBadRequest:
writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{err.Error()}})
return putFixRoute, http.StatusBadRequest
default:
if err == commonerr.ErrNotFound {
writeResponse(w, r, http.StatusNotFound, FeatureEnvelope{Error: &Error{err.Error()}})
return putFixRoute, http.StatusNotFound
}
writeResponse(w, r, http.StatusInternalServerError, FeatureEnvelope{Error: &Error{err.Error()}})
return putFixRoute, http.StatusInternalServerError
}
}
writeResponse(w, r, http.StatusOK, FeatureEnvelope{Feature: request.Feature})
return putFixRoute, http.StatusOK
}
func deleteFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
err := ctx.Store.DeleteVulnerabilityFix(p.ByName("vulnerabilityNamespace"), p.ByName("vulnerabilityName"), p.ByName("fixName"))
if err == commonerr.ErrNotFound {
writeResponse(w, r, http.StatusNotFound, FeatureEnvelope{Error: &Error{err.Error()}})
return deleteFixRoute, http.StatusNotFound
} else if err != nil {
writeResponse(w, r, http.StatusInternalServerError, FeatureEnvelope{Error: &Error{err.Error()}})
return deleteFixRoute, http.StatusInternalServerError
}
w.WriteHeader(http.StatusOK)
return deleteFixRoute, http.StatusOK
}
func getNotification(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
query := r.URL.Query()
limitStrs, limitExists := query["limit"]
if !limitExists {
writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"must provide limit query parameter"}})
return getNotificationRoute, http.StatusBadRequest
}
limit, err := strconv.Atoi(limitStrs[0])
if err != nil {
writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"invalid limit format: " + err.Error()}})
return getNotificationRoute, http.StatusBadRequest
}
var pageToken string
page := database.VulnerabilityNotificationFirstPage
pageStrs, pageExists := query["page"]
if pageExists {
err := tokenUnmarshal(pageStrs[0], ctx.PaginationKey, &page)
if err != nil {
writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"invalid page format: " + err.Error()}})
return getNotificationRoute, http.StatusBadRequest
}
pageToken = pageStrs[0]
} else {
pageTokenBytes, err := tokenMarshal(page, ctx.PaginationKey)
if err != nil {
writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"failed to marshal token: " + err.Error()}})
return getNotificationRoute, http.StatusBadRequest
}
pageToken = string(pageTokenBytes)
}
dbNotification, nextPage, err := ctx.Store.GetNotification(p.ByName("notificationName"), limit, page)
if err == commonerr.ErrNotFound {
writeResponse(w, r, http.StatusNotFound, NotificationEnvelope{Error: &Error{err.Error()}})
return deleteNotificationRoute, http.StatusNotFound
} else if err != nil {
writeResponse(w, r, http.StatusInternalServerError, NotificationEnvelope{Error: &Error{err.Error()}})
return getNotificationRoute, http.StatusInternalServerError
}
notification := NotificationFromDatabaseModel(dbNotification, limit, pageToken, nextPage, ctx.PaginationKey)
writeResponse(w, r, http.StatusOK, NotificationEnvelope{Notification: &notification})
return getNotificationRoute, http.StatusOK
}
func deleteNotification(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
err := ctx.Store.DeleteNotification(p.ByName("notificationName"))
if err == commonerr.ErrNotFound {
writeResponse(w, r, http.StatusNotFound, NotificationEnvelope{Error: &Error{err.Error()}})
return deleteNotificationRoute, http.StatusNotFound
} else if err != nil {
writeResponse(w, r, http.StatusInternalServerError, NotificationEnvelope{Error: &Error{err.Error()}})
return deleteNotificationRoute, http.StatusInternalServerError
}
w.WriteHeader(http.StatusOK)
return deleteNotificationRoute, http.StatusOK
}
func getMetrics(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) {
prometheus.Handler().ServeHTTP(w, r)
return getMetricsRoute, 0
}

@ -0,0 +1,7 @@
FROM golang:alpine
RUN apk add --update --no-cache git bash protobuf-dev
RUN go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
RUN go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
RUN go get -u github.com/golang/protobuf/protoc-gen-go

File diff suppressed because it is too large Load Diff

@ -0,0 +1,442 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: api/v3/clairpb/clair.proto
/*
Package clairpb is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package clairpb
import (
"io"
"net/http"
"github.com/golang/protobuf/proto"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/grpc-ecosystem/grpc-gateway/utilities"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/status"
)
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
func request_AncestryService_GetAncestry_0(ctx context.Context, marshaler runtime.Marshaler, client AncestryServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetAncestryRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["ancestry_name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "ancestry_name")
}
protoReq.AncestryName, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "ancestry_name", err)
}
msg, err := client.GetAncestry(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func request_AncestryService_PostAncestry_0(ctx context.Context, marshaler runtime.Marshaler, client AncestryServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq PostAncestryRequest
var metadata runtime.ServerMetadata
if req.ContentLength > 0 {
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
}
msg, err := client.PostAncestry(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
var (
filter_NotificationService_GetNotification_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
)
func request_NotificationService_GetNotification_0(ctx context.Context, marshaler runtime.Marshaler, client NotificationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetNotificationRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_NotificationService_GetNotification_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetNotification(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func request_NotificationService_MarkNotificationAsRead_0(ctx context.Context, marshaler runtime.Marshaler, client NotificationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq MarkNotificationAsReadRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := client.MarkNotificationAsRead(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func request_StatusService_GetStatus_0(ctx context.Context, marshaler runtime.Marshaler, client StatusServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetStatusRequest
var metadata runtime.ServerMetadata
msg, err := client.GetStatus(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
// RegisterAncestryServiceHandlerFromEndpoint is same as RegisterAncestryServiceHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterAncestryServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.Dial(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterAncestryServiceHandler(ctx, mux, conn)
}
// RegisterAncestryServiceHandler registers the http handlers for service AncestryService to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterAncestryServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterAncestryServiceHandlerClient(ctx, mux, NewAncestryServiceClient(conn))
}
// RegisterAncestryServiceHandler registers the http handlers for service AncestryService to "mux".
// The handlers forward requests to the grpc endpoint over the given implementation of "AncestryServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AncestryServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "AncestryServiceClient" to call the correct interceptors.
func RegisterAncestryServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AncestryServiceClient) error {
mux.Handle("GET", pattern_AncestryService_GetAncestry_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_AncestryService_GetAncestry_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_AncestryService_GetAncestry_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_AncestryService_PostAncestry_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_AncestryService_PostAncestry_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_AncestryService_PostAncestry_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_AncestryService_GetAncestry_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1}, []string{"ancestry", "ancestry_name"}, ""))
pattern_AncestryService_PostAncestry_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"ancestry"}, ""))
)
var (
forward_AncestryService_GetAncestry_0 = runtime.ForwardResponseMessage
forward_AncestryService_PostAncestry_0 = runtime.ForwardResponseMessage
)
// RegisterNotificationServiceHandlerFromEndpoint is same as RegisterNotificationServiceHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterNotificationServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.Dial(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterNotificationServiceHandler(ctx, mux, conn)
}
// RegisterNotificationServiceHandler registers the http handlers for service NotificationService to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterNotificationServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterNotificationServiceHandlerClient(ctx, mux, NewNotificationServiceClient(conn))
}
// RegisterNotificationServiceHandler registers the http handlers for service NotificationService to "mux".
// The handlers forward requests to the grpc endpoint over the given implementation of "NotificationServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "NotificationServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "NotificationServiceClient" to call the correct interceptors.
func RegisterNotificationServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client NotificationServiceClient) error {
mux.Handle("GET", pattern_NotificationService_GetNotification_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_NotificationService_GetNotification_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_NotificationService_GetNotification_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("DELETE", pattern_NotificationService_MarkNotificationAsRead_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_NotificationService_MarkNotificationAsRead_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_NotificationService_MarkNotificationAsRead_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_NotificationService_GetNotification_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1}, []string{"notifications", "name"}, ""))
pattern_NotificationService_MarkNotificationAsRead_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1}, []string{"notifications", "name"}, ""))
)
var (
forward_NotificationService_GetNotification_0 = runtime.ForwardResponseMessage
forward_NotificationService_MarkNotificationAsRead_0 = runtime.ForwardResponseMessage
)
// RegisterStatusServiceHandlerFromEndpoint is same as RegisterStatusServiceHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterStatusServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.Dial(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterStatusServiceHandler(ctx, mux, conn)
}
// RegisterStatusServiceHandler registers the http handlers for service StatusService to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterStatusServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterStatusServiceHandlerClient(ctx, mux, NewStatusServiceClient(conn))
}
// RegisterStatusServiceHandler registers the http handlers for service StatusService to "mux".
// The handlers forward requests to the grpc endpoint over the given implementation of "StatusServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "StatusServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "StatusServiceClient" to call the correct interceptors.
func RegisterStatusServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client StatusServiceClient) error {
mux.Handle("GET", pattern_StatusService_GetStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_StatusService_GetStatus_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_StatusService_GetStatus_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_StatusService_GetStatus_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"status"}, ""))
)
var (
forward_StatusService_GetStatus_0 = runtime.ForwardResponseMessage
)

@ -0,0 +1,248 @@
// Copyright 2018 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto";
package coreos.clair;
option go_package = "clairpb";
option java_package = "com.coreos.clair.pb";
service AncestryService {
// The RPC used to read the results of scanning for a particular ancestry.
rpc GetAncestry(GetAncestryRequest) returns (GetAncestryResponse) {
option (google.api.http) = { get: "/ancestry/{ancestry_name}" };
}
// The RPC used to create a new scan of an ancestry.
rpc PostAncestry(PostAncestryRequest) returns (PostAncestryResponse) {
option (google.api.http) = {
post: "/ancestry"
body: "*"
};
}
}
service StatusService {
// The RPC used to show the internal state of current Clair instance.
rpc GetStatus(GetStatusRequest) returns (GetStatusResponse) {
option (google.api.http) = { get: "/status" };
}
}
service NotificationService {
// The RPC used to get a particularly Notification.
rpc GetNotification(GetNotificationRequest) returns (GetNotificationResponse) {
option (google.api.http) = { get: "/notifications/{name}" };
}
// The RPC used to mark a Notification as read after it has been processed.
rpc MarkNotificationAsRead(MarkNotificationAsReadRequest) returns (MarkNotificationAsReadResponse) {
option (google.api.http) = { delete: "/notifications/{name}" };
}
}
message Vulnerability {
// The name of the vulnerability.
string name = 1;
// The name of the namespace in which the vulnerability was detected.
string namespace_name = 2;
// A description of the vulnerability according to the source for the namespace.
string description = 3;
// A link to the vulnerability according to the source for the namespace.
string link = 4;
// How dangerous the vulnerability is.
string severity = 5;
// Namespace agnostic metadata about the vulnerability.
string metadata = 6;
// The feature that fixes this vulnerability.
// This field only exists when a vulnerability is a part of a Feature.
string fixed_by = 7;
// The Features that are affected by the vulnerability.
// This field only exists when a vulnerability is a part of a Notification.
repeated Feature affected_versions = 8;
}
message Detector {
enum DType {
DETECTOR_D_TYPE_INVALID = 0;
DETECTOR_D_TYPE_NAMESPACE = 1;
DETECTOR_D_TYPE_FEATURE = 2;
}
// The name of the detector.
string name = 1;
// The version of the detector.
string version = 2;
// The type of the detector.
DType dtype = 3;
}
message Namespace {
// The name of the namespace.
string name = 1;
// The detector used to detect the namespace. This only exists when present in
// an Ancestry Feature.
Detector detector = 2;
}
message Feature {
// The name of the feature.
string name = 1;
// The namespace in which the feature is detected.
Namespace namespace = 2;
// The specific version of this feature.
string version = 3;
// The format used to parse version numbers for the feature.
string version_format = 4;
// The detector used to detect this feature. This only exists when present in
// an Ancestry.
Detector detector = 5;
// The list of vulnerabilities that affect the feature.
repeated Vulnerability vulnerabilities = 6;
// The feature type indicates if the feature represents a source package or
// binary package.
string feature_type = 7;
}
message Layer {
// The sha256 tarsum for the layer.
string hash = 1;
}
message ClairStatus {
// The implemented detectors in this Clair instance
repeated Detector detectors = 1;
// The time at which the updater last ran.
google.protobuf.Timestamp last_update_time = 2;
}
message GetAncestryRequest {
// The name of the desired ancestry.
string ancestry_name = 1;
}
message GetAncestryResponse {
message AncestryLayer {
// The layer's information.
Layer layer = 1;
// The features detected in this layer.
repeated Feature detected_features = 2;
}
message Ancestry {
// The name of the desired ancestry.
string name = 1;
// The detectors used to scan this Ancestry. It may not be the current set
// of detectors in clair status.
repeated Detector detectors = 2;
// The list of layers along with detected features in each.
repeated AncestryLayer layers = 3;
}
// The ancestry requested.
Ancestry ancestry = 1;
// The status of Clair at the time of the request.
ClairStatus status = 2;
}
message PostAncestryRequest {
message PostLayer {
// The hash of the layer.
string hash = 1;
// The location of the layer (URL or file path).
string path = 2;
// Any HTTP Headers that need to be used if requesting a layer over HTTP(S).
map<string, string> headers = 3;
}
// The name of the ancestry being scanned.
// If scanning OCI images, this should be the hash of the manifest.
string ancestry_name = 1;
// The format of the image being uploaded.
string format = 2;
// The layers to be scanned for this Ancestry, ordered in the way that i th
// layer is the parent of i + 1 th layer.
repeated PostLayer layers = 3;
}
message PostAncestryResponse {
// The status of Clair at the time of the request.
ClairStatus status = 1;
}
message GetNotificationRequest {
// The current page of previous vulnerabilities for the ancestry.
// This will be empty when it is the first page.
string old_vulnerability_page = 1;
// The current page of vulnerabilities for the ancestry.
// This will be empty when it is the first page.
string new_vulnerability_page = 2;
// The requested maximum number of results per page.
int32 limit = 3;
// The name of the notification being requested.
string name = 4;
}
message GetNotificationResponse {
message Notification {
// The name of the requested notification.
string name = 1;
// The time at which the notification was created.
string created = 2;
// The time at which the notification was last sent out.
string notified = 3;
// The time at which a notification has been deleted.
string deleted = 4;
// The previous vulnerability and a paginated view of the ancestries it affects.
PagedVulnerableAncestries old = 5;
// The newly updated vulnerability and a paginated view of the ancestries it affects.
PagedVulnerableAncestries new = 6;
}
// The notification as requested.
Notification notification = 1;
}
message PagedVulnerableAncestries {
message IndexedAncestryName {
// The index is an ever increasing number associated with the particular ancestry.
// This is useful if you're processing notifications, and need to keep track of the progress of paginating the results.
int32 index = 1;
// The name of the ancestry.
string name = 2;
}
// The identifier for the current page.
string current_page = 1;
// The token used to request the next page.
// This will be empty when there are no more pages.
string next_page = 2;
// The requested maximum number of results per page.
int32 limit = 3;
// The vulnerability that affects a given set of ancestries.
Vulnerability vulnerability = 4;
// The ancestries affected by a vulnerability.
repeated IndexedAncestryName ancestries = 5;
}
message MarkNotificationAsReadRequest {
// The name of the Notification that has been processed.
string name = 1;
}
message MarkNotificationAsReadResponse {}
message GetStatusRequest {}
message GetStatusResponse {
// The status of the current Clair instance.
ClairStatus status = 1;
}

@ -0,0 +1,495 @@
{
"swagger": "2.0",
"info": {
"title": "api/v3/clairpb/clair.proto",
"version": "version not set"
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/ancestry": {
"post": {
"summary": "The RPC used to create a new scan of an ancestry.",
"operationId": "PostAncestry",
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/clairPostAncestryResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/clairPostAncestryRequest"
}
}
],
"tags": [
"AncestryService"
]
}
},
"/ancestry/{ancestry_name}": {
"get": {
"summary": "The RPC used to read the results of scanning for a particular ancestry.",
"operationId": "GetAncestry",
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/clairGetAncestryResponse"
}
}
},
"parameters": [
{
"name": "ancestry_name",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"AncestryService"
]
}
},
"/notifications/{name}": {
"get": {
"summary": "The RPC used to get a particularly Notification.",
"operationId": "GetNotification",
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/clairGetNotificationResponse"
}
}
},
"parameters": [
{
"name": "name",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "old_vulnerability_page",
"description": "The current page of previous vulnerabilities for the ancestry.\nThis will be empty when it is the first page.",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "new_vulnerability_page",
"description": "The current page of vulnerabilities for the ancestry.\nThis will be empty when it is the first page.",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "limit",
"description": "The requested maximum number of results per page.",
"in": "query",
"required": false,
"type": "integer",
"format": "int32"
}
],
"tags": [
"NotificationService"
]
},
"delete": {
"summary": "The RPC used to mark a Notification as read after it has been processed.",
"operationId": "MarkNotificationAsRead",
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/clairMarkNotificationAsReadResponse"
}
}
},
"parameters": [
{
"name": "name",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"NotificationService"
]
}
},
"/status": {
"get": {
"summary": "The RPC used to show the internal state of current Clair instance.",
"operationId": "GetStatus",
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/clairGetStatusResponse"
}
}
},
"tags": [
"StatusService"
]
}
}
},
"definitions": {
"DetectorDType": {
"type": "string",
"enum": [
"DETECTOR_D_TYPE_INVALID",
"DETECTOR_D_TYPE_NAMESPACE",
"DETECTOR_D_TYPE_FEATURE"
],
"default": "DETECTOR_D_TYPE_INVALID"
},
"GetAncestryResponseAncestry": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the desired ancestry."
},
"detectors": {
"type": "array",
"items": {
"$ref": "#/definitions/clairDetector"
},
"description": "The detectors used to scan this Ancestry. It may not be the current set\nof detectors in clair status."
},
"layers": {
"type": "array",
"items": {
"$ref": "#/definitions/GetAncestryResponseAncestryLayer"
},
"description": "The list of layers along with detected features in each."
}
}
},
"GetAncestryResponseAncestryLayer": {
"type": "object",
"properties": {
"layer": {
"$ref": "#/definitions/clairLayer",
"description": "The layer's information."
},
"detected_features": {
"type": "array",
"items": {
"$ref": "#/definitions/clairFeature"
},
"description": "The features detected in this layer."
}
}
},
"GetNotificationResponseNotification": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the requested notification."
},
"created": {
"type": "string",
"description": "The time at which the notification was created."
},
"notified": {
"type": "string",
"description": "The time at which the notification was last sent out."
},
"deleted": {
"type": "string",
"description": "The time at which a notification has been deleted."
},
"old": {
"$ref": "#/definitions/clairPagedVulnerableAncestries",
"description": "The previous vulnerability and a paginated view of the ancestries it affects."
},
"new": {
"$ref": "#/definitions/clairPagedVulnerableAncestries",
"description": "The newly updated vulnerability and a paginated view of the ancestries it affects."
}
}
},
"PagedVulnerableAncestriesIndexedAncestryName": {
"type": "object",
"properties": {
"index": {
"type": "integer",
"format": "int32",
"description": "The index is an ever increasing number associated with the particular ancestry.\nThis is useful if you're processing notifications, and need to keep track of the progress of paginating the results."
},
"name": {
"type": "string",
"description": "The name of the ancestry."
}
}
},
"PostAncestryRequestPostLayer": {
"type": "object",
"properties": {
"hash": {
"type": "string",
"description": "The hash of the layer."
},
"path": {
"type": "string",
"description": "The location of the layer (URL or filepath)."
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Any HTTP Headers that need to be used if requesting a layer over HTTP(S)."
}
}
},
"clairClairStatus": {
"type": "object",
"properties": {
"detectors": {
"type": "array",
"items": {
"$ref": "#/definitions/clairDetector"
},
"title": "The implemented detectors in this Clair instance"
},
"last_update_time": {
"type": "string",
"format": "date-time",
"description": "The time at which the updater last ran."
}
}
},
"clairDetector": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the detector."
},
"version": {
"type": "string",
"description": "The version of the detector."
},
"dtype": {
"$ref": "#/definitions/DetectorDType",
"description": "The type of the detector."
}
}
},
"clairFeature": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the feature."
},
"namespace": {
"$ref": "#/definitions/clairNamespace",
"description": "The namespace in which the feature is detected."
},
"version": {
"type": "string",
"description": "The specific version of this feature."
},
"version_format": {
"type": "string",
"description": "The format used to parse version numbers for the feature."
},
"detector": {
"$ref": "#/definitions/clairDetector",
"description": "The detector used to detect this feature. This only exists when present in\nan Ancestry."
},
"vulnerabilities": {
"type": "array",
"items": {
"$ref": "#/definitions/clairVulnerability"
},
"description": "The list of vulnerabilities that affect the feature."
},
"feature_type": {
"type": "string",
"description": "The feature type indicates if the feature represents a source package or\nbinary package."
}
}
},
"clairGetAncestryResponse": {
"type": "object",
"properties": {
"ancestry": {
"$ref": "#/definitions/GetAncestryResponseAncestry",
"description": "The ancestry requested."
},
"status": {
"$ref": "#/definitions/clairClairStatus",
"description": "The status of Clair at the time of the request."
}
}
},
"clairGetNotificationResponse": {
"type": "object",
"properties": {
"notification": {
"$ref": "#/definitions/GetNotificationResponseNotification",
"description": "The notification as requested."
}
}
},
"clairGetStatusResponse": {
"type": "object",
"properties": {
"status": {
"$ref": "#/definitions/clairClairStatus",
"description": "The status of the current Clair instance."
}
}
},
"clairLayer": {
"type": "object",
"properties": {
"hash": {
"type": "string",
"description": "The sha256 tarsum for the layer."
}
}
},
"clairMarkNotificationAsReadResponse": {
"type": "object"
},
"clairNamespace": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the namespace."
},
"detector": {
"$ref": "#/definitions/clairDetector",
"description": "The detector used to detect the namespace. This only exists when present in\nan Ancestry Feature."
}
}
},
"clairPagedVulnerableAncestries": {
"type": "object",
"properties": {
"current_page": {
"type": "string",
"description": "The identifier for the current page."
},
"next_page": {
"type": "string",
"description": "The token used to request the next page.\nThis will be empty when there are no more pages."
},
"limit": {
"type": "integer",
"format": "int32",
"description": "The requested maximum number of results per page."
},
"vulnerability": {
"$ref": "#/definitions/clairVulnerability",
"description": "The vulnerability that affects a given set of ancestries."
},
"ancestries": {
"type": "array",
"items": {
"$ref": "#/definitions/PagedVulnerableAncestriesIndexedAncestryName"
},
"description": "The ancestries affected by a vulnerability."
}
}
},
"clairPostAncestryRequest": {
"type": "object",
"properties": {
"ancestry_name": {
"type": "string",
"description": "The name of the ancestry being scanned.\nIf scanning OCI images, this should be the hash of the manifest."
},
"format": {
"type": "string",
"description": "The format of the image being uploaded."
},
"layers": {
"type": "array",
"items": {
"$ref": "#/definitions/PostAncestryRequestPostLayer"
},
"description": "The layers to be scanned for this Ancestry, ordered in the way that i th\nlayer is the parent of i + 1 th layer."
}
}
},
"clairPostAncestryResponse": {
"type": "object",
"properties": {
"status": {
"$ref": "#/definitions/clairClairStatus",
"description": "The status of Clair at the time of the request."
}
}
},
"clairVulnerability": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the vulnerability."
},
"namespace_name": {
"type": "string",
"description": "The name of the namespace in which the vulnerability was detected."
},
"description": {
"type": "string",
"description": "A description of the vulnerability according to the source for the namespace."
},
"link": {
"type": "string",
"description": "A link to the vulnerability according to the source for the namespace."
},
"severity": {
"type": "string",
"description": "How dangerous the vulnerability is."
},
"metadata": {
"type": "string",
"description": "Namespace agnostic metadata about the vulnerability."
},
"fixed_by": {
"type": "string",
"description": "The feature that fixes this vulnerability.\nThis field only exists when a vulnerability is a part of a Feature."
},
"affected_versions": {
"type": "array",
"items": {
"$ref": "#/definitions/clairFeature"
},
"description": "The Features that are affected by the vulnerability.\nThis field only exists when a vulnerability is a part of a Notification."
}
}
}
}
}

@ -0,0 +1,174 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clairpb
import (
"encoding/json"
"fmt"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
)
// DatabaseDetectorTypeMapping maps the database detector type to the integer
// enum proto.
var DatabaseDetectorTypeMapping = map[database.DetectorType]Detector_DType{
database.NamespaceDetectorType: Detector_DType(1),
database.FeatureDetectorType: Detector_DType(2),
}
// PagedVulnerableAncestriesFromDatabaseModel converts database
// PagedVulnerableAncestries to api PagedVulnerableAncestries and assigns
// indexes to ancestries.
func PagedVulnerableAncestriesFromDatabaseModel(dbVuln *database.PagedVulnerableAncestries) (*PagedVulnerableAncestries, error) {
if dbVuln == nil {
return nil, nil
}
vuln, err := VulnerabilityFromDatabaseModel(dbVuln.Vulnerability)
if err != nil {
return nil, err
}
next := ""
if !dbVuln.End {
next = string(dbVuln.Next)
}
vulnAncestry := PagedVulnerableAncestries{
Vulnerability: vuln,
CurrentPage: string(dbVuln.Current),
NextPage: next,
Limit: int32(dbVuln.Limit),
}
for index, ancestryName := range dbVuln.Affected {
indexedAncestry := PagedVulnerableAncestries_IndexedAncestryName{
Name: ancestryName,
Index: int32(index),
}
vulnAncestry.Ancestries = append(vulnAncestry.Ancestries, &indexedAncestry)
}
return &vulnAncestry, nil
}
// NotificationFromDatabaseModel converts database notification, old and new
// vulnerabilities' paged vulnerable ancestries to be api notification.
func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotificationWithVulnerable) (*GetNotificationResponse_Notification, error) {
var (
noti GetNotificationResponse_Notification
err error
)
noti.Name = dbNotification.Name
if !dbNotification.Created.IsZero() {
noti.Created = fmt.Sprintf("%d", dbNotification.Created.Unix())
}
if !dbNotification.Notified.IsZero() {
noti.Notified = fmt.Sprintf("%d", dbNotification.Notified.Unix())
}
if !dbNotification.Deleted.IsZero() {
noti.Deleted = fmt.Sprintf("%d", dbNotification.Deleted.Unix())
}
noti.Old, err = PagedVulnerableAncestriesFromDatabaseModel(dbNotification.Old)
if err != nil {
return nil, err
}
noti.New, err = PagedVulnerableAncestriesFromDatabaseModel(dbNotification.New)
if err != nil {
return nil, err
}
return &noti, nil
}
// VulnerabilityFromDatabaseModel converts database Vulnerability to api Vulnerability.
func VulnerabilityFromDatabaseModel(dbVuln database.Vulnerability) (*Vulnerability, error) {
metaString := ""
if dbVuln.Metadata != nil {
metadataByte, err := json.Marshal(dbVuln.Metadata)
if err != nil {
return nil, err
}
metaString = string(metadataByte)
}
return &Vulnerability{
Name: dbVuln.Name,
NamespaceName: dbVuln.Namespace.Name,
Description: dbVuln.Description,
Link: dbVuln.Link,
Severity: string(dbVuln.Severity),
Metadata: metaString,
}, nil
}
// VulnerabilityWithFixedInFromDatabaseModel converts database VulnerabilityWithFixedIn to api Vulnerability.
func VulnerabilityWithFixedInFromDatabaseModel(dbVuln database.VulnerabilityWithFixedIn) (*Vulnerability, error) {
vuln, err := VulnerabilityFromDatabaseModel(dbVuln.Vulnerability)
if err != nil {
return nil, err
}
vuln.FixedBy = dbVuln.FixedInVersion
return vuln, nil
}
// NamespacedFeatureFromDatabaseModel converts database namespacedFeature to api Feature.
func NamespacedFeatureFromDatabaseModel(feature database.AncestryFeature) *Feature {
version := feature.Feature.Version
if version == versionfmt.MaxVersion {
version = "None"
}
return &Feature{
Name: feature.Feature.Name,
Namespace: &Namespace{
Name: feature.Namespace.Name,
Detector: DetectorFromDatabaseModel(feature.NamespaceBy),
},
VersionFormat: feature.Namespace.VersionFormat,
Version: version,
Detector: DetectorFromDatabaseModel(feature.FeatureBy),
FeatureType: string(feature.Type),
}
}
// DetectorFromDatabaseModel converts database detector to api detector.
func DetectorFromDatabaseModel(detector database.Detector) *Detector {
if !detector.Valid() {
return nil
}
return &Detector{
Name: detector.Name,
Version: detector.Version,
Dtype: DatabaseDetectorTypeMapping[detector.DType],
}
}
// DetectorsFromDatabaseModel converts database detectors to api detectors.
func DetectorsFromDatabaseModel(dbDetectors []database.Detector) []*Detector {
detectors := make([]*Detector, 0, len(dbDetectors))
for _, d := range dbDetectors {
detectors = append(detectors, DetectorFromDatabaseModel(d))
}
return detectors
}

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Copyright 2018 clair authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -o errexit
set -o nounset
set -o pipefail
DOCKER_REPO_ROOT="$GOPATH/src/github.com/coreos/clair"
IMAGE=${IMAGE:-"quay.io/coreos/clair-gen-proto"}
docker run --rm -it \
-v "$DOCKER_REPO_ROOT":"$DOCKER_REPO_ROOT" \
-w "$DOCKER_REPO_ROOT" \
"$IMAGE" \
"./api/v3/clairpb/run_in_docker.sh"

@ -0,0 +1,3 @@
protoc_version: 3.5.1
protoc_includes:
- ../../../vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Copyright 2018 clair authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -o errexit
set -o nounset
set -o pipefail
protoc -I/usr/include -I. \
-I"${GOPATH}/src" \
-I"${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis" \
--go_out=plugins=grpc:. \
--grpc-gateway_out=logtostderr=true:. \
--swagger_out=logtostderr=true:. \
./api/v3/clairpb/clair.proto
go generate .

@ -0,0 +1,232 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v3
import (
"sync"
"golang.org/x/net/context"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/coreos/clair"
pb "github.com/coreos/clair/api/v3/clairpb"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/imagefmt"
"github.com/coreos/clair/pkg/pagination"
)
func newRPCErrorWithClairError(code codes.Code, err error) error {
return status.Errorf(code, "clair error reason: '%s'", err.Error())
}
// NotificationServer implements NotificationService interface for serving RPC.
type NotificationServer struct {
Store database.Datastore
}
// AncestryServer implements AncestryService interface for serving RPC.
type AncestryServer struct {
Store database.Datastore
}
// StatusServer implements StatusService interface for serving RPC.
type StatusServer struct {
Store database.Datastore
}
// GetStatus implements getting the current status of Clair via the Clair service.
func (s *StatusServer) GetStatus(ctx context.Context, req *pb.GetStatusRequest) (*pb.GetStatusResponse, error) {
clairStatus, err := GetClairStatus(s.Store)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &pb.GetStatusResponse{Status: clairStatus}, nil
}
// PostAncestry implements posting an ancestry via the Clair gRPC service.
func (s *AncestryServer) PostAncestry(ctx context.Context, req *pb.PostAncestryRequest) (*pb.PostAncestryResponse, error) {
blobFormat := req.GetFormat()
if !imagefmt.IsSupported(blobFormat) {
return nil, status.Error(codes.InvalidArgument, "image blob format is not supported")
}
clairStatus, err := GetClairStatus(s.Store)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
// check if the ancestry is already processed; if not we build the ancestry again.
layerHashes := make([]string, len(req.Layers))
for i, layer := range req.Layers {
layerHashes[i] = layer.GetHash()
}
found, err := clair.IsAncestryCached(s.Store, req.AncestryName, layerHashes)
if err != nil {
return nil, newRPCErrorWithClairError(codes.Internal, err)
}
if found {
return &pb.PostAncestryResponse{Status: clairStatus}, nil
}
builder := clair.NewAncestryBuilder(clair.EnabledDetectors())
layerMap := map[string]*database.Layer{}
layerMapLock := sync.RWMutex{}
g, analyzerCtx := errgroup.WithContext(ctx)
for i := range req.Layers {
layer := req.Layers[i]
if _, ok := layerMap[layer.Hash]; !ok {
layerMap[layer.Hash] = nil
if layer == nil {
err := status.Error(codes.InvalidArgument, "ancestry layer is invalid")
return nil, err
}
if layer.GetHash() == "" {
return nil, status.Error(codes.InvalidArgument, "ancestry layer hash should not be empty")
}
if layer.GetPath() == "" {
return nil, status.Error(codes.InvalidArgument, "ancestry layer path should not be empty")
}
g.Go(func() error {
clairLayer, err := clair.AnalyzeLayer(analyzerCtx, s.Store, layer.Hash, req.Format, layer.Path, layer.Headers)
if err != nil {
return err
}
layerMapLock.Lock()
layerMap[layer.Hash] = clairLayer
layerMapLock.Unlock()
return nil
})
}
}
if err = g.Wait(); err != nil {
return nil, newRPCErrorWithClairError(codes.Internal, err)
}
for _, layer := range req.Layers {
builder.AddLeafLayer(layerMap[layer.Hash])
}
if err := clair.SaveAncestry(s.Store, builder.Ancestry(req.AncestryName)); err != nil {
return nil, newRPCErrorWithClairError(codes.Internal, err)
}
return &pb.PostAncestryResponse{Status: clairStatus}, nil
}
// GetAncestry implements retrieving an ancestry via the Clair gRPC service.
func (s *AncestryServer) GetAncestry(ctx context.Context, req *pb.GetAncestryRequest) (*pb.GetAncestryResponse, error) {
name := req.GetAncestryName()
if name == "" {
return nil, status.Errorf(codes.InvalidArgument, "ancestry name should not be empty")
}
ancestry, ok, err := database.FindAncestryAndRollback(s.Store, name)
if err != nil {
return nil, newRPCErrorWithClairError(codes.Internal, err)
}
if !ok {
return nil, status.Errorf(codes.NotFound, "requested ancestry '%s' is not found", req.GetAncestryName())
}
pbAncestry := &pb.GetAncestryResponse_Ancestry{
Name: ancestry.Name,
Detectors: pb.DetectorsFromDatabaseModel(ancestry.By),
}
for _, layer := range ancestry.Layers {
pbLayer, err := s.GetPbAncestryLayer(layer)
if err != nil {
return nil, err
}
pbAncestry.Layers = append(pbAncestry.Layers, pbLayer)
}
pbClairStatus, err := GetClairStatus(s.Store)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &pb.GetAncestryResponse{
Status: pbClairStatus,
Ancestry: pbAncestry,
}, nil
}
// GetNotification implements retrieving a notification via the Clair gRPC
// service.
func (s *NotificationServer) GetNotification(ctx context.Context, req *pb.GetNotificationRequest) (*pb.GetNotificationResponse, error) {
if req.GetName() == "" {
return nil, status.Error(codes.InvalidArgument, "notification name should not be empty")
}
if req.GetLimit() <= 0 {
return nil, status.Error(codes.InvalidArgument, "notification page limit should not be empty or less than 1")
}
dbNotification, ok, err := database.FindVulnerabilityNotificationAndRollback(
s.Store,
req.GetName(),
int(req.GetLimit()),
pagination.Token(req.GetOldVulnerabilityPage()),
pagination.Token(req.GetNewVulnerabilityPage()),
)
if err != nil {
return nil, newRPCErrorWithClairError(codes.Internal, err)
}
if !ok {
return nil, status.Errorf(codes.NotFound, "requested notification '%s' is not found", req.GetName())
}
notification, err := pb.NotificationFromDatabaseModel(dbNotification)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &pb.GetNotificationResponse{Notification: notification}, nil
}
// MarkNotificationAsRead implements deleting a notification via the Clair gRPC
// service.
func (s *NotificationServer) MarkNotificationAsRead(ctx context.Context, req *pb.MarkNotificationAsReadRequest) (*pb.MarkNotificationAsReadResponse, error) {
if req.GetName() == "" {
return nil, status.Error(codes.InvalidArgument, "notification name should not be empty")
}
found, err := database.MarkNotificationAsReadAndCommit(s.Store, req.GetName())
if err != nil {
return nil, newRPCErrorWithClairError(codes.Internal, err)
}
if !found {
return nil, status.Errorf(codes.NotFound, "requested notification '%s' is not found", req.GetName())
}
return &pb.MarkNotificationAsReadResponse{}, nil
}

@ -0,0 +1,105 @@
// Copyright 2018 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v3
import (
"net/http"
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
pb "github.com/coreos/clair/api/v3/clairpb"
"github.com/coreos/clair/database"
"github.com/coreos/clair/pkg/grpcutil"
)
var (
promResponseDurationMilliseconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "clair_v3_api_response_duration_milliseconds",
Help: "The duration of time it takes to receive and write a response to an V2 API request",
Buckets: prometheus.ExponentialBuckets(9.375, 2, 10),
}, []string{"route", "code"})
)
func init() {
prometheus.MustRegister(promResponseDurationMilliseconds)
}
func prometheusHandler(h http.Handler) http.Handler {
mux := http.NewServeMux()
mux.Handle("/", h)
mux.Handle("/metrics", prometheus.Handler())
return mux
}
type httpStatusWriter struct {
http.ResponseWriter
StatusCode int
}
func (w *httpStatusWriter) WriteHeader(code int) {
w.StatusCode = code
w.ResponseWriter.WriteHeader(code)
}
func loggingHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lrw := &httpStatusWriter{ResponseWriter: w, StatusCode: http.StatusOK}
h.ServeHTTP(lrw, r)
log.WithFields(log.Fields{
"remote addr": r.RemoteAddr,
"method": r.Method,
"request uri": r.RequestURI,
"status": strconv.Itoa(lrw.StatusCode),
"elapsed time (ms)": float64(time.Since(start).Nanoseconds()) * 1e-6,
}).Info("handled HTTP request")
})
}
// ListenAndServe serves the Clair v3 API over gRPC and the gRPC Gateway.
func ListenAndServe(addr, certFile, keyFile, caPath string, store database.Datastore) error {
srv := grpcutil.MuxedGRPCServer{
Addr: addr,
ServicesFunc: func(gsrv *grpc.Server) {
pb.RegisterAncestryServiceServer(gsrv, &AncestryServer{Store: store})
pb.RegisterNotificationServiceServer(gsrv, &NotificationServer{Store: store})
pb.RegisterStatusServiceServer(gsrv, &StatusServer{Store: store})
},
ServiceHandlerFuncs: []grpcutil.RegisterServiceHandlerFunc{
pb.RegisterAncestryServiceHandler,
pb.RegisterNotificationServiceHandler,
pb.RegisterStatusServiceHandler,
},
}
middleware := func(h http.Handler) http.Handler {
return prometheusHandler(loggingHandler(h))
}
var err error
if caPath == "" {
err = srv.ListenAndServe(middleware)
} else {
err = srv.ListenAndServeTLS(certFile, keyFile, caPath, middleware)
}
return err
}

@ -0,0 +1,92 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v3
import (
"github.com/coreos/clair"
pb "github.com/coreos/clair/api/v3/clairpb"
"github.com/coreos/clair/database"
"github.com/golang/protobuf/ptypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// GetClairStatus retrieves the current status of Clair and wrap it inside
// protobuf struct.
func GetClairStatus(store database.Datastore) (*pb.ClairStatus, error) {
status := &pb.ClairStatus{
Detectors: pb.DetectorsFromDatabaseModel(clair.EnabledDetectors()),
}
t, firstUpdate, err := clair.GetLastUpdateTime(store)
if err != nil {
return nil, err
}
if firstUpdate {
return status, nil
}
status.LastUpdateTime, err = ptypes.TimestampProto(t)
if err != nil {
return nil, err
}
return status, nil
}
// GetPbAncestryLayer retrieves an ancestry layer with vulnerabilities and
// features in an ancestry based on the provided database layer.
func (s *AncestryServer) GetPbAncestryLayer(layer database.AncestryLayer) (*pb.GetAncestryResponse_AncestryLayer, error) {
pbLayer := &pb.GetAncestryResponse_AncestryLayer{
Layer: &pb.Layer{
Hash: layer.Hash,
},
}
features := layer.GetFeatures()
affectedFeatures, err := database.FindAffectedNamespacedFeaturesAndRollback(s.Store, features)
if err != nil {
return nil, newRPCErrorWithClairError(codes.Internal, err)
}
for _, feature := range affectedFeatures {
if !feature.Valid {
panic("feature is missing in the database, it indicates the database is corrupted.")
}
for _, detectedFeature := range layer.Features {
if detectedFeature.NamespacedFeature != feature.NamespacedFeature {
continue
}
var (
pbFeature = pb.NamespacedFeatureFromDatabaseModel(detectedFeature)
pbVuln *pb.Vulnerability
err error
)
for _, vuln := range feature.AffectedBy {
if pbVuln, err = pb.VulnerabilityWithFixedInFromDatabaseModel(vuln); err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
pbFeature.Vulnerabilities = append(pbFeature.Vulnerabilities, pbVuln)
}
pbLayer.DetectedFeatures = append(pbLayer.DetectedFeatures, pbFeature)
}
}
return pbLayer, nil
}

@ -5,25 +5,25 @@
"confidence": 1
},
{
"project": "github.com/coreos/go-systemd/journal",
"license": "Apache License 2.0",
"confidence": 0.997
"project": "github.com/beorn7/perks/quantile",
"license": "MIT License",
"confidence": 0.989
},
{
"project": "github.com/coreos/pkg",
"project": "github.com/coreos/pkg/timeutil",
"license": "Apache License 2.0",
"confidence": 1
},
{
"project": "github.com/davecgh/go-spew/spew",
"license": "ISC License",
"confidence": 1
},
{
"project": "github.com/golang/protobuf/proto",
"license": "BSD 3-clause \"New\" or \"Revised\" License",
"confidence": 0.92
},
{
"project": "github.com/google/uuid",
"license": "BSD 3-clause \"New\" or \"Revised\" License",
"confidence": 0.966
},
{
"project": "github.com/matttproud/golang_protobuf_extensions/pbutil",
"license": "Apache License 2.0",
@ -34,11 +34,6 @@
"license": "BSD 3-clause \"New\" or \"Revised\" License",
"confidence": 0.966
},
{
"project": "github.com/pmezard/go-difflib/difflib",
"license": "BSD 3-clause \"New\" or \"Revised\" License",
"confidence": 0.983
},
{
"project": "github.com/prometheus/client_golang/prometheus",
"license": "Apache License 2.0",
@ -55,13 +50,28 @@
"confidence": 1
},
{
"project": "github.com/prometheus/procfs",
"project": "github.com/prometheus/procfs/xfs",
"license": "Apache License 2.0",
"confidence": 1
},
{
"project": "github.com/sirupsen/logrus",
"license": "MIT License",
"confidence": 1
},
{
"project": "github.com/stretchr/testify/assert",
"license": "MIT License",
"confidence": 0.943
},
{
"project": "github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew",
"license": "ISC License",
"confidence": 0.985
},
{
"project": "github.com/stretchr/testify/vendor/github.com/pmezard/go-difflib/difflib",
"license": "BSD 3-clause \"New\" or \"Revised\" License",
"confidence": 0.983
}
]

@ -0,0 +1,43 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clair
import (
"context"
"io"
"net/http"
"os"
"strings"
"github.com/coreos/clair/pkg/httputil"
)
func retrieveLayerBlob(ctx context.Context, path string, headers map[string]string) (io.ReadCloser, error) {
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
httpHeaders := make(http.Header)
for key, value := range headers {
httpHeaders[key] = []string{value}
}
reader, err := httputil.GetWithContext(ctx, path, httpHeaders)
if err != nil {
return nil, err
}
return reader, nil
}
return os.Open(path)
}

@ -1,4 +1,4 @@
// Copyright 2017 clair authors
// Copyright 2018 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -20,13 +20,15 @@ import (
"os"
"time"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"github.com/coreos/clair"
"github.com/coreos/clair/api"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/notification"
"github.com/fernet/fernet-go"
"github.com/coreos/clair/ext/vulnsrc"
"github.com/coreos/clair/pkg/pagination"
)
// ErrDatasourceNotLoaded is returned when the datasource variable in the
@ -54,11 +56,12 @@ func DefaultConfig() Config {
Type: "pgsql",
},
Updater: &clair.UpdaterConfig{
Interval: 1 * time.Hour,
EnabledUpdaters: vulnsrc.ListUpdaters(),
Interval: 1 * time.Hour,
},
API: &api.Config{
Port: 6060,
HealthPort: 6061,
HealthAddr: "0.0.0.0:6061",
Addr: "0.0.0.0:6060",
Timeout: 900 * time.Second,
},
Notifier: &notification.Config{
@ -96,16 +99,12 @@ func LoadConfig(path string) (config *Config, err error) {
config = &cfgFile.Clair
// Generate a pagination key if none is provided.
if config.API.PaginationKey == "" {
var key fernet.Key
if err = key.Generate(); err != nil {
return
}
config.API.PaginationKey = key.Encode()
if v, ok := config.Database.Options["paginationkey"]; !ok || v == nil || v.(string) == "" {
log.Warn("pagination key is empty, generating...")
config.Database.Options["paginationkey"] = pagination.Must(pagination.NewKey()).String()
} else {
_, err = fernet.DecodeKey(config.API.PaginationKey)
_, err = pagination.KeyFromString(config.Database.Options["paginationkey"].(string))
if err != nil {
err = errors.New("Invalid Pagination key; must be 32-bit URL-safe base64")
return
}
}

@ -1,4 +1,4 @@
// Copyright 2017 clair authors
// Copyright 2018 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -30,9 +30,10 @@ import (
"github.com/coreos/clair"
"github.com/coreos/clair/api"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/imagefmt"
"github.com/coreos/clair/ext/vulnsrc"
"github.com/coreos/clair/pkg/formatter"
"github.com/coreos/clair/pkg/stopper"
"github.com/coreos/clair/pkg/strutil"
// Register database driver.
_ "github.com/coreos/clair/database/pgsql"
@ -51,12 +52,26 @@ import (
_ "github.com/coreos/clair/ext/notification/webhook"
_ "github.com/coreos/clair/ext/vulnmdsrc/nvd"
_ "github.com/coreos/clair/ext/vulnsrc/alpine"
_ "github.com/coreos/clair/ext/vulnsrc/amzn"
_ "github.com/coreos/clair/ext/vulnsrc/debian"
_ "github.com/coreos/clair/ext/vulnsrc/oracle"
_ "github.com/coreos/clair/ext/vulnsrc/rhel"
_ "github.com/coreos/clair/ext/vulnsrc/suse"
_ "github.com/coreos/clair/ext/vulnsrc/ubuntu"
)
// MaxDBConnectionAttempts is the total number of tries that Clair will use to
// initially connect to a database at start-up.
const MaxDBConnectionAttempts = 20
// BinaryDependencies are the programs that Clair expects to be on the $PATH
// because it creates subprocesses of these programs.
var BinaryDependencies = []string{
"git",
"rpm",
"xz",
}
func waitForSignals(signals ...os.Signal) {
interrupts := make(chan os.Signal, 1)
signal.Notify(interrupts, signals...)
@ -85,25 +100,46 @@ func stopCPUProfiling(f *os.File) {
log.Info("stopped CPU profiling")
}
func configClairVersion(config *Config) {
clair.EnabledUpdaters = strutil.Intersect(config.Updater.EnabledUpdaters, vulnsrc.ListUpdaters())
log.WithFields(log.Fields{
"Detectors": database.SerializeDetectors(clair.EnabledDetectors()),
"Updaters": clair.EnabledUpdaters,
}).Info("enabled Clair extensions")
}
// Boot starts Clair instance with the provided config.
func Boot(config *Config) {
rand.Seed(time.Now().UnixNano())
st := stopper.NewStopper()
// Open database
db, err := database.Open(config.Database)
if err != nil {
log.Fatal(err)
var db database.Datastore
var dbError error
for attempts := 1; attempts <= MaxDBConnectionAttempts; attempts++ {
db, dbError = database.Open(config.Database)
if dbError == nil {
break
}
log.WithError(dbError).Error("failed to connect to database")
time.Sleep(time.Duration(attempts) * time.Second)
}
if dbError != nil {
log.Fatal(dbError)
}
defer db.Close()
clair.RegisterConfiguredDetectors(db)
// Start notifier
st.Begin()
go clair.RunNotifier(config.Notifier, db, st)
// Start API
st.Begin()
go api.Run(config.API, db, st)
go api.Run(config.API, db)
st.Begin()
go api.RunHealth(config.API, db, st)
@ -117,46 +153,47 @@ func Boot(config *Config) {
st.Stop()
}
// Initialize logging system
func configureLogger(flagLogLevel *string) {
logLevel, err := log.ParseLevel(strings.ToUpper(*flagLogLevel))
if err != nil {
log.WithError(err).Error("failed to set logger parser level")
}
log.SetLevel(logLevel)
log.SetOutput(os.Stdout)
log.SetFormatter(&formatter.JSONExtendedFormatter{ShowLn: true})
}
func main() {
// Parse command-line arguments
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
flagConfigPath := flag.String("config", "/etc/clair/config.yaml", "Load configuration from the specified file.")
flagCPUProfilePath := flag.String("cpu-profile", "", "Write a CPU profile to the specified file before exiting.")
flagLogLevel := flag.String("log-level", "info", "Define the logging level.")
flagInsecureTLS := flag.Bool("insecure-tls", false, "Disable TLS server's certificate chain and hostname verification when pulling layers.")
flag.Parse()
configureLogger(flagLogLevel)
// Check for dependencies.
for _, bin := range []string{"git", "rpm", "xz"} {
for _, bin := range BinaryDependencies {
_, err := exec.LookPath(bin)
if err != nil {
log.WithError(err).WithField("dependency", bin).Fatal("failed to find dependency")
}
}
// Load configuration
config, err := LoadConfig(*flagConfigPath)
if err != nil {
log.WithError(err).Fatal("failed to load configuration")
}
// Initialize logging system
logLevel, err := log.ParseLevel(strings.ToUpper(*flagLogLevel))
log.SetLevel(logLevel)
log.SetOutput(os.Stdout)
log.SetFormatter(&formatter.JSONExtendedFormatter{ShowLn: true})
// Enable CPU Profiling if specified
if *flagCPUProfilePath != "" {
defer stopCPUProfiling(startCPUProfiling(*flagCPUProfilePath))
}
// Enable TLS server's certificate chain and hostname verification
// when pulling layers if specified
if *flagInsecureTLS {
imagefmt.SetInsecureTLS(*flagInsecureTLS)
}
// configure updater and worker
configClairVersion(config)
Boot(config)
}

@ -0,0 +1,61 @@
## CoreOS Community Code of Conduct
### Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating
documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free
experience for everyone, regardless of level of experience, gender, gender
identity and expression, sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing others' private information, such as physical or electronic addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct. By adopting this Code of Conduct,
project maintainers commit themselves to fairly and consistently applying these
principles to every aspect of managing this project. Project maintainers who do
not follow or enforce the Code of Conduct may be permanently removed from the
project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting a project maintainer, Brandon Philips
<brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.
This Code of Conduct is adapted from the Contributor Covenant
(http://contributor-covenant.org), version 1.2.0, available at
http://contributor-covenant.org/version/1/2/0/
### CoreOS Events Code of Conduct
CoreOS events are working conferences intended for professional networking and
collaboration in the CoreOS community. Attendees are expected to behave
according to professional standards and in accordance with their employers
policies on appropriate workplace behavior.
While at CoreOS events or related social networking opportunities, attendees
should not engage in discriminatory or offensive speech or actions including
but not limited to gender, sexuality, race, age, disability, or religion.
Speakers should be especially aware of these concerns.
CoreOS does not condone any statements by speakers contrary to these standards.
CoreOS reserves the right to deny entrance and/or eject from an event (without
refund) any individual found to be engaging in discriminatory or offensive
speech or actions.
Please bring any concerns to the immediate attention of designated on-site
staff, Brandon Philips <brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.

@ -26,22 +26,22 @@ clair:
# Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database.
cachesize: 16384
# 32-bit URL-safe base64 key used to encrypt pagination tokens
# If one is not provided, it will be generated.
# Multiple clair instances in the same cluster need the same value.
paginationkey:
api:
# API server port
port: 6060
# v3 grpc/RESTful API server address
addr: "0.0.0.0:6060"
# Health server port
# Health server address
# This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server.
healthport: 6061
healthaddr: "0.0.0.0:6061"
# Deadline before an API request will respond with a 503
timeout: 900s
# 32-bit URL-safe base64 key used to encrypt pagination tokens
# If one is not provided, it will be generated.
# Multiple clair instances in the same cluster need the same value.
paginationkey:
# Optional PKI configuration
# If you want to easily generate client certificates and CAs, try the following projects:
# https://github.com/coreos/etcd-ca
@ -55,6 +55,13 @@ clair:
# Frequency the database will be updated with vulnerabilities from the default data sources
# The value 0 disables the updater entirely.
interval: 2h
enabledupdaters:
- debian
- ubuntu
- rhel
- oracle
- alpine
- suse
notifier:
# Number of attempts before the notification is marked as failed to be sent
@ -72,9 +79,9 @@ clair:
# https://github.com/cloudflare/cfssl
# https://github.com/coreos/etcd-ca
servername:
cafile:
keyfile:
certfile:
cafile:
keyfile:
certfile:
# Optional HTTP Proxy: must be a valid URL (including the scheme).
proxy:

@ -0,0 +1,21 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj

@ -0,0 +1,11 @@
name: clair
home: https://coreos.com/clair
version: 0.1.1
appVersion: 3.0.0-pre
description: Clair is an open source project for the static analysis of vulnerabilities in application containers.
icon: https://cloud.githubusercontent.com/assets/343539/21630811/c5081e5c-d202-11e6-92eb-919d5999c77a.png
sources:
- https://github.com/coreos/clair
maintainers:
- name: Jimmy Zelinskie
email: jimmy.zelinskie@coreos.com

@ -0,0 +1,5 @@
dependencies:
- name: postgresql
version: "1.0.0"
condition: postgresql.enabled
repository: "alias:stable"

@ -0,0 +1,83 @@
clair:
database:
# Database driver.
type: pgsql
options:
# PostgreSQL Connection string.
# https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
{{- if .Values.config.postgresURI }}
source: "{{ .Values.config.postgresURI }}"
{{ else }}
source: "host={{ template "postgresql.fullname" . }} port=5432 user={{ .Values.postgresql.postgresUser }} password={{ .Values.postgresql.postgresPassword }} dbname={{ .Values.postgresql.postgresDatabase }} sslmode=disable statement_timeout=60000"
{{ end }}
# Number of elements kept in the cache.
# Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database.
cachesize: 16384
# 32-bit URL-safe base64 key used to encrypt pagination tokens.
# If one is not provided, it will be generated.
# Multiple clair instances in the same cluster need the same value.
paginationkey: "{{ .Values.config.paginationKey }}"
api:
# v3 grpc/RESTful API server address.
addr: "0.0.0.0:{{ .Values.service.internalApiPort }}"
# Health server address.
# This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server.
healthaddr: "0.0.0.0:{{ .Values.service.internalHealthPort }}"
# Deadline before an API request will respond with a 503.
timeout: 900s
# Optional PKI configuration.
# If you want to easily generate client certificates and CAs, try the following projects:
# https://github.com/coreos/etcd-ca
# https://github.com/cloudflare/cfssl
servername:
cafile:
keyfile:
certfile:
worker:
namespace_detectors:
{{- range $key, $value := .Values.config.enabledNamespaceDetectors }}
- {{ $value }}
{{- end }}
feature_listers:
{{- range $key, $value := .Values.config.enabledFeatureListers }}
- {{ $value }}
{{- end }}
updater:
# Frequency the database will be updated with vulnerabilities from the default data sources.
# The value 0 disables the updater entirely.
interval: "{{ .Values.config.updateInterval }}"
enabledupdaters:
{{- range $key, $value := .Values.config.enabledUpdaters }}
- {{ $value }}
{{- end }}
notifier:
# Number of attempts before the notification is marked as failed to be sent.
attempts: 3
# Duration before a failed notification is retried.
renotifyinterval: 2h
http:
# Optional endpoint that will receive notifications via POST requests.
endpoint: "{{ .Values.config.notificationWebhookEndpoint }}"
# Optional PKI configuration.
# If you want to easily generate client certificates and CAs, try the following projects:
# https://github.com/cloudflare/cfssl
# https://github.com/coreos/etcd-ca
servername:
cafile:
keyfile:
certfile:
# Optional HTTP Proxy: must be a valid URL (including the scheme).
proxy:

@ -0,0 +1,33 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "clair.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "clair.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create a default fully qualified postgresql name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
*/}}
{{- define "postgresql.fullname" -}}
{{- printf "%s-%s" .Release.Name "postgresql" | trunc 63 | trimSuffix "-" -}}
{{- end -}}

@ -0,0 +1,59 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ template "clair.fullname" . }}
labels:
heritage: {{ .Release.Service | quote }}
release: {{ .Release.Name | quote }}
chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
app: {{ template "clair.fullname" . }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ template "clair.fullname" . }}
template:
metadata:
labels:
app: {{ template "clair.fullname" . }}
spec:
volumes:
- name: "{{ .Chart.Name }}-config"
secret:
secretName: {{ template "clair.fullname" . }}
{{- if .Values.nodeSelector }}
nodeSelector:
{{ toYaml .Values.nodeSelector | indent 8 }}
{{- end }}
{{- if .Values.tolerations }}
tolerations:
{{ toYaml .Values.tolerations | indent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- "-log-level={{ .Values.logLevel }}"
{{ if .Values.insecureTls }}- "--insecure-tls" {{end}}
ports:
- name: "{{ .Chart.Name }}-api"
containerPort: {{ .Values.service.internalApiPort }}
protocol: TCP
- name: "{{ .Chart.Name }}-health"
containerPort: {{ .Values.service.internalHealthPort }}
protocol: TCP
livenessProbe:
httpGet:
path: /health
port: {{ .Values.service.internalHealthPort }}
readinessProbe:
httpGet:
path: /health
port: {{ .Values.service.internalHealthPort }}
volumeMounts:
- name: "{{ .Chart.Name }}-config"
mountPath: /etc/clair
readOnly: true
resources:
{{ toYaml .Values.resources | indent 10 }}

@ -0,0 +1,33 @@
{{- if .Values.ingress.enabled -}}
{{- $serviceName := include "clair.fullname" . -}}
{{- $servicePort := .Values.service.externalApiPort -}}
{{- $path := .Values.ingress.path | default "/" -}}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: {{ template "clair.fullname" . }}
labels:
heritage: {{ .Release.Service | quote }}
release: {{ .Release.Name | quote }}
chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
app: {{ template "clair.fullname" . }}
annotations:
{{- range $key, $value := .Values.ingress.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
spec:
rules:
{{- range $host := .Values.ingress.hosts }}
- host: {{ $host }}
http:
paths:
- path: {{ $path }}
backend:
serviceName: {{ $serviceName }}
servicePort: {{ $servicePort }}
{{- end -}}
{{- if .Values.ingress.tls }}
tls:
{{ toYaml .Values.ingress.tls | indent 4 }}
{{- end -}}
{{- end -}}

@ -0,0 +1,13 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ template "clair.fullname" . }}
labels:
heritage: {{ .Release.Service | quote }}
release: {{ .Release.Name | quote }}
chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
app: {{ template "clair.fullname" . }}
type: Opaque
data:
config.yaml: |-
{{ include (print .Template.BasePath "/_config.yaml.tpl") . | b64enc | indent 4 }}

@ -0,0 +1,28 @@
apiVersion: v1
kind: Service
metadata:
name: {{ template "clair.fullname" . }}
labels:
heritage: {{ .Release.Service | quote }}
release: {{ .Release.Name | quote }}
chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
app: {{ template "clair.fullname" . }}
spec:
type: {{ .Values.service.type }}
ports:
- name: "{{ .Chart.Name }}-api"
port: {{ .Values.service.externalApiPort }}
targetPort: {{ .Values.service.internalApiPort }}
protocol: TCP
{{- if and (.Values.service.apiNodePort) (eq .Values.service.type "NodePort") }}
nodePort: {{ .Values.service.apiNodePort }}
{{- end }}
- name: "{{ .Chart.Name }}-health"
port: {{ .Values.service.externalHealthPort }}
targetPort: {{ .Values.service.internalHealthPort }}
protocol: TCP
{{- if and (.Values.service.healthNodePort) (eq .Values.service.type "NodePort") }}
nodePort: {{ .Values.service.healthNodePort }}
{{- end }}
selector:
app: {{ template "clair.fullname" . }}

@ -0,0 +1,79 @@
# Default values for clair.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
logLevel: info
insecureTls: false
image:
repository: quay.io/coreos/clair-git
tag: latest
pullPolicy: Always
service:
type: ClusterIP
internalApiPort: 6060
externalApiPort: 6060
internalHealthPort: 6061
externalHealthPort: 6061
ingress:
enabled: false
# Used to create Ingress record (should used with service.type: ClusterIP).
hosts:
- chart-example.local
annotations:
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
tls:
# Secrets must be manually created in the namespace.
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources:
limits:
cpu: 200m
memory: 1500Mi
requests:
cpu: 100m
memory: 500Mi
config:
# postgresURI: "postgres://user:password@host:5432/postgres?sslmode=disable"
paginationKey: "XxoPtCUzrUv4JV5dS+yQ+MdW7yLEJnRMwigVY/bpgtQ="
updateInterval: 2h
notificationWebhookEndpoint: https://example.com/notify/me
enabledUpdaters:
- debian
- ubuntu
- rhel
- oracle
- alpine
enabledNamespaceDetectors:
- os-release
- lsb-release
- apt-sources
- alpine-release
- redhat-release
enabledFeatureListers:
- apk
- dpkg
- rpm
nodeSelector: {}
tolerations: []
# Configuration values for the postgresql dependency.
# ref: https://github.com/kubernetes/charts/blob/master/stable/postgresql/README.md
postgresql:
# The dependant Postgres chart can be disabled, to connect to
# an existing database by defining config.postgresURI
enabled: true
imageTag: 9.6-alpine
cpu: 1000m
memory: 1Gi
# These values are hardcoded until Helm supports secrets.
# For more info see: https://github.com/kubernetes/helm/issues/2196
postgresUser: clair
postgresPassword: clair
postgresDatabase: clair
persistence:
size: 10Gi

@ -1,84 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: clairsvc
labels:
app: clair
spec:
type: NodePort
ports:
- port: 6060
protocol: TCP
nodePort: 30060
name: clair-port0
- port: 6061
protocol: TCP
nodePort: 30061
name: clair-port1
selector:
app: clair
---
apiVersion: v1
kind: ReplicationController
metadata:
name: clair
spec:
replicas: 1
template:
metadata:
labels:
app: clair
spec:
volumes:
- name: secret-volume
secret:
secretName: clairsecret
containers:
- name: clair
image: quay.io/coreos/clair
args:
- "-config"
- "/config/config.yaml"
ports:
- containerPort: 6060
- containerPort: 6061
volumeMounts:
- mountPath: /config
name: secret-volume
---
apiVersion: v1
kind: ReplicationController
metadata:
labels:
app: postgres
name: clair-postgres
spec:
replicas: 1
selector:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- image: postgres:latest
name: postgres
env:
- name: POSTGRES_PASSWORD
value: password
ports:
- containerPort: 5432
name: postgres-port
---
apiVersion: v1
kind: Service
metadata:
labels:
app: postgres
name: postgres
spec:
ports:
- port: 5432
selector:
app: postgres

@ -1,80 +0,0 @@
# Copyright 2015 clair authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# The values specified here are the default values that Clair uses if no configuration file is specified or if the keys are not defined.
clair:
database:
# Database driver
type: pgsql
options:
# PostgreSQL Connection string
# https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
source: postgres://postgres:password@postgres:5432/postgres?sslmode=disable
# Number of elements kept in the cache
# Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database.
cachesize: 16384
api:
# API server port
port: 6060
# Health server port
# This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server.
healthport: 6061
# Deadline before an API request will respond with a 503
timeout: 900s
# 32-bit URL-safe base64 key used to encrypt pagination tokens
# If one is not provided, it will be generated.
# Multiple clair instances in the same cluster need the same value.
paginationkey:
# Optional PKI configuration
# If you want to easily generate client certificates and CAs, try the following projects:
# https://github.com/coreos/etcd-ca
# https://github.com/cloudflare/cfssl
servername:
cafile:
keyfile:
certfile:
updater:
# Frequency the database will be updated with vulnerabilities from the default data sources
# The value 0 disables the updater entirely.
interval: 2h
notifier:
# Number of attempts before the notification is marked as failed to be sent
attempts: 3
# Duration before a failed notification is retried
renotifyinterval: 2h
http:
# Optional endpoint that will receive notifications via POST requests
endpoint:
# Optional PKI configuration
# If you want to easily generate client certificates and CAs, try the following projects:
# https://github.com/cloudflare/cfssl
# https://github.com/coreos/etcd-ca
servername:
cafile:
keyfile:
certfile:
# Optional HTTP Proxy: must be a valid URL (including the scheme).
proxy:

@ -0,0 +1,96 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
// Ancestry is a manifest that keeps all layers in an image in order.
type Ancestry struct {
// Name is a globally unique value for a set of layers. This is often the
// sha256 digest of an OCI/Docker manifest.
Name string `json:"name"`
// By contains the processors that are used when computing the
// content of this ancestry.
By []Detector `json:"by"`
// Layers should be ordered and i_th layer is the parent of i+1_th layer in
// the slice.
Layers []AncestryLayer `json:"layers"`
}
// Valid checks if the ancestry is compliant to spec.
func (a *Ancestry) Valid() bool {
if a == nil {
return false
}
if a.Name == "" {
return false
}
for _, d := range a.By {
if !d.Valid() {
return false
}
}
for _, l := range a.Layers {
if !l.Valid() {
return false
}
}
return true
}
// AncestryLayer is a layer with all detected namespaced features.
type AncestryLayer struct {
// Hash is the sha-256 tarsum on the layer's blob content.
Hash string `json:"hash"`
// Features are the features introduced by this layer when it was
// processed.
Features []AncestryFeature `json:"features"`
}
// Valid checks if the Ancestry Layer is compliant to the spec.
func (l *AncestryLayer) Valid() bool {
if l == nil {
return false
}
if l.Hash == "" {
return false
}
return true
}
// GetFeatures returns the Ancestry's features.
func (l *AncestryLayer) GetFeatures() []NamespacedFeature {
nsf := make([]NamespacedFeature, 0, len(l.Features))
for _, f := range l.Features {
nsf = append(nsf, f.NamespacedFeature)
}
return nsf
}
// AncestryFeature is a namespaced feature with the detectors used to
// find this feature.
type AncestryFeature struct {
NamespacedFeature `json:"namespacedFeature"`
// FeatureBy is the detector that detected the feature.
FeatureBy Detector `json:"featureBy"`
// NamespaceBy is the detector that detected the namespace.
NamespaceBy Detector `json:"namespaceBy"`
}

@ -1,4 +1,4 @@
// Copyright 2017 clair authors
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -17,20 +17,29 @@
package database
import (
"errors"
"fmt"
"time"
"github.com/coreos/clair/pkg/pagination"
)
var (
// ErrBackendException is an error that occurs when the database backend does
// not work properly (ie. unreachable).
ErrBackendException = errors.New("database: an error occured when querying the backend")
// ErrBackendException is an error that occurs when the database backend
// does not work properly (ie. unreachable).
ErrBackendException = NewStorageError("an error occurred when querying the backend")
// ErrInconsistent is an error that occurs when a database consistency check
// fails (i.e. when an entity which is supposed to be unique is detected
// twice)
ErrInconsistent = errors.New("database: inconsistent database")
ErrInconsistent = NewStorageError("inconsistent database")
// ErrInvalidParameters is an error that occurs when the parameters are not valid.
ErrInvalidParameters = NewStorageError("parameters are not valid")
// ErrMissingEntities is an error that occurs when an associated immutable
// entity doesn't exist in the database. This error can indicate a wrong
// implementation or corrupted database.
ErrMissingEntities = NewStorageError("associated immutable entities are missing in the database")
)
// RegistrableComponentConfig is a configuration block that can be used to
@ -43,8 +52,8 @@ type RegistrableComponentConfig struct {
var drivers = make(map[string]Driver)
// Driver is a function that opens a Datastore specified by its database driver type and specific
// configuration.
// Driver is a function that opens a Datastore specified by its database driver
// type and specific configuration.
type Driver func(RegistrableComponentConfig) (Datastore, error)
// Register makes a Constructor available by the provided name.
@ -70,148 +79,131 @@ func Open(cfg RegistrableComponentConfig) (Datastore, error) {
return driver(cfg)
}
// Datastore represents the required operations on a persistent data store for
// a Clair deployment.
type Datastore interface {
// ListNamespaces returns the entire list of known Namespaces.
ListNamespaces() ([]Namespace, error)
// InsertLayer stores a Layer in the database.
//
// A Layer is uniquely identified by its Name.
// The Name and EngineVersion fields are mandatory.
// If a Parent is specified, it is expected that it has been retrieved using
// FindLayer.
// If a Layer that already exists is inserted and the EngineVersion of the
// given Layer is higher than the stored one, the stored Layer should be
// updated.
// The function has to be idempotent, inserting a layer that already exists
// shouldn't return an error.
InsertLayer(Layer) error
// FindLayer retrieves a Layer from the database.
//
// When `withFeatures` is true, the Features field should be filled.
// When `withVulnerabilities` is true, the Features field should be filled
// and their AffectedBy fields should contain every vulnerabilities that
// affect them.
FindLayer(name string, withFeatures, withVulnerabilities bool) (Layer, error)
// DeleteLayer deletes a Layer from the database and every layers that are
// based on it, recursively.
DeleteLayer(name string) error
// ListVulnerabilities returns the list of vulnerabilities of a particular
// Namespace.
//
// The Limit and page parameters are used to paginate the return list.
// The first given page should be 0.
// The function should return the next available page. If there are no more
// pages, -1 has to be returned.
ListVulnerabilities(namespaceName string, limit int, page int) ([]Vulnerability, int, error)
// InsertVulnerabilities stores the given Vulnerabilities in the database,
// updating them if necessary.
//
// A vulnerability is uniquely identified by its Namespace and its Name.
// The FixedIn field may only contain a partial list of Features that are
// affected by the Vulnerability, along with the version in which the
// vulnerability is fixed. It is the responsibility of the implementation to
// update the list properly.
// A version equals to versionfmt.MinVersion means that the given Feature is
// not being affected by the Vulnerability at all and thus, should be removed
// from the list.
// It is important that Features should be unique in the FixedIn list. For
// example, it doesn't make sense to have two `openssl` Feature listed as a
// Vulnerability can only be fixed in one Version. This is true because
// Vulnerabilities and Features are namespaced (i.e. specific to one
// operating system).
// Each vulnerability insertion or update has to create a Notification that
// will contain the old and the updated Vulnerability, unless
// createNotification equals to true.
InsertVulnerabilities(vulnerabilities []Vulnerability, createNotification bool) error
// FindVulnerability retrieves a Vulnerability from the database, including
// the FixedIn list.
FindVulnerability(namespaceName, name string) (Vulnerability, error)
// DeleteVulnerability removes a Vulnerability from the database.
// Session contains the required operations on a persistent data store for a
// Clair deployment.
//
// Session is started by Datastore.Begin and terminated with Commit or Rollback.
// Besides Commit and Rollback, other functions cannot be called after the
// session is terminated.
// Any function is not guaranteed to be called successfully if there's a session
// failure.
type Session interface {
// Commit commits changes to datastore.
//
// It has to create a Notification that will contain the old Vulnerability.
DeleteVulnerability(namespaceName, name string) error
// Commit call after Rollback does no-op.
Commit() error
// InsertVulnerabilityFixes adds new FixedIn Feature or update the Versions
// of existing ones to the specified Vulnerability in the database.
// Rollback drops changes to datastore.
//
// It has has to create a Notification that will contain the old and the
// updated Vulnerability.
InsertVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName string, fixes []FeatureVersion) error
// Rollback call after Commit does no-op.
Rollback() error
// UpsertAncestry inserts or replaces an ancestry and its namespaced
// features and processors used to scan the ancestry.
UpsertAncestry(Ancestry) error
// FindAncestry retrieves an ancestry with all detected
// namespaced features. If the ancestry is not found, return false.
FindAncestry(name string) (ancestry Ancestry, found bool, err error)
// DeleteVulnerabilityFix removes a FixedIn Feature from the specified
// Vulnerability in the database. It can be used to store the fact that a
// Vulnerability no longer affects the given Feature in any Version.
// PersistDetector inserts a slice of detectors if not in the database.
PersistDetectors(detectors []Detector) error
// PersistFeatures inserts a set of features if not in the database.
PersistFeatures(features []Feature) error
// PersistNamespacedFeatures inserts a set of namespaced features if not in
// the database.
PersistNamespacedFeatures([]NamespacedFeature) error
// CacheAffectedNamespacedFeatures relates the namespaced features with the
// vulnerabilities affecting these features.
//
// It has has to create a Notification that will contain the old and the
// updated Vulnerability.
DeleteVulnerabilityFix(vulnerabilityNamespace, vulnerabilityName, featureName string) error
// NOTE(Sida): it's not necessary for every database implementation and so
// this function may have a better home.
CacheAffectedNamespacedFeatures([]NamespacedFeature) error
// FindAffectedNamespacedFeatures retrieves a set of namespaced features
// with affecting vulnerabilities.
FindAffectedNamespacedFeatures(features []NamespacedFeature) ([]NullableAffectedNamespacedFeature, error)
// GetAvailableNotification returns the Name, Created, Notified and Deleted
// fields of a Notification that should be handled.
// PersistNamespaces inserts a set of namespaces if not in the database.
PersistNamespaces([]Namespace) error
// PersistLayer appends a layer's content in the database.
//
// The renotify interval defines how much time after being marked as Notified
// by SetNotificationNotified, a Notification that hasn't been deleted should
// be returned again by this function.
// A Notification for which there is a valid Lock with the same Name should
// not be returned.
GetAvailableNotification(renotifyInterval time.Duration) (VulnerabilityNotification, error)
// GetNotification returns a Notification, including its OldVulnerability and
// NewVulnerability fields.
// If any feature, namespace, or detector is not in the database, it returns not found error.
PersistLayer(hash string, features []LayerFeature, namespaces []LayerNamespace, detectedBy []Detector) error
// FindLayer returns a layer with all detected features and
// namespaces.
FindLayer(hash string) (layer Layer, found bool, err error)
// InsertVulnerabilities inserts a set of UNIQUE vulnerabilities with
// affected features into database, assuming that all vulnerabilities
// provided are NOT in database and all vulnerabilities' namespaces are
// already in the database.
InsertVulnerabilities([]VulnerabilityWithAffected) error
// FindVulnerability retrieves a set of Vulnerabilities with affected
// features.
FindVulnerabilities([]VulnerabilityID) ([]NullableVulnerability, error)
// DeleteVulnerability removes a set of Vulnerabilities assuming that the
// requested vulnerabilities are in the database.
DeleteVulnerabilities([]VulnerabilityID) error
// InsertVulnerabilityNotifications inserts a set of unique vulnerability
// notifications into datastore, assuming that they are not in the database.
InsertVulnerabilityNotifications([]VulnerabilityNotification) error
// FindNewNotification retrieves a notification, which has never been
// notified or notified before a certain time.
FindNewNotification(notifiedBefore time.Time) (hook NotificationHook, found bool, err error)
// FindVulnerabilityNotification retrieves a vulnerability notification with
// affected ancestries affected by old or new vulnerability.
//
// On these Vulnerabilities, LayersIntroducingVulnerability should be filled
// with every Layer that introduces the Vulnerability (i.e. adds at least one
// affected FeatureVersion).
// The Limit and page parameters are used to paginate
// LayersIntroducingVulnerability. The first given page should be
// VulnerabilityNotificationFirstPage. The function will then return the next
// available page. If there is no more page, NoVulnerabilityNotificationPage
// has to be returned.
GetNotification(name string, limit int, page VulnerabilityNotificationPageNumber) (VulnerabilityNotification, VulnerabilityNotificationPageNumber, error)
// SetNotificationNotified marks a Notification as notified and thus, makes
// it unavailable for GetAvailableNotification, until the renotify duration
// is elapsed.
SetNotificationNotified(name string) error
// DeleteNotification marks a Notification as deleted, and thus, makes it
// unavailable for GetAvailableNotification.
// Because the number of affected ancestries maybe large, they are paginated
// and their pages are specified by the pagination token, which should be
// considered first page when it's empty.
FindVulnerabilityNotification(name string, limit int, oldVulnerabilityPage pagination.Token, newVulnerabilityPage pagination.Token) (noti VulnerabilityNotificationWithVulnerable, found bool, err error)
// MarkNotificationAsRead marks a Notification as notified now, assuming
// the requested notification is in the database.
MarkNotificationAsRead(name string) error
// DeleteNotification removes a Notification in the database.
DeleteNotification(name string) error
// InsertKeyValue stores or updates a simple key/value pair in the database.
InsertKeyValue(key, value string) error
// UpdateKeyValue stores or updates a simple key/value pair.
UpdateKeyValue(key, value string) error
// GetKeyValue retrieves a value from the database from the given key.
// FindKeyValue retrieves a value from the given key.
FindKeyValue(key string) (value string, found bool, err error)
// AcquireLock acquires a brand new lock in the database with a given name
// for the given duration.
//
// It returns an empty string if there is no such key.
GetKeyValue(key string) (string, error)
// A lock can only have one owner.
// This method should NOT block until a lock is acquired.
AcquireLock(name, owner string, duration time.Duration) (acquired bool, expiration time.Time, err error)
// Lock creates or renew a Lock in the database with the given name, owner
// and duration.
// ExtendLock extends an existing lock such that the lock will expire at the
// current time plus the provided duration.
//
// After the specified duration, the Lock expires by itself if it hasn't been
// unlocked, and thus, let other users create a Lock with the same name.
// However, the owner can renew its Lock by setting renew to true.
// Lock should not block, it should instead returns whether the Lock has been
// successfully acquired/renewed. If it's the case, the expiration time of
// that Lock is returned as well.
Lock(name string, owner string, duration time.Duration, renew bool) (bool, time.Time)
// Unlock releases an existing Lock.
Unlock(name, owner string)
// FindLock returns the owner of a Lock specified by the name, and its
// expiration time if it exists.
FindLock(name string) (string, time.Time, error)
// This method should return immediately with an error if the lock does not
// exist.
ExtendLock(name, owner string, duration time.Duration) (extended bool, expiration time.Time, err error)
// ReleaseLock releases an existing lock.
ReleaseLock(name, owner string) error
}
// Datastore represents a persistent data store
type Datastore interface {
// Begin starts a session to change.
Begin() (Session, error)
// Ping returns the health status of the database.
Ping() bool

@ -0,0 +1,539 @@
// Copyright 2018 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
import (
"encoding/json"
"time"
log "github.com/sirupsen/logrus"
"github.com/coreos/clair/pkg/commonerr"
"github.com/coreos/clair/pkg/pagination"
"github.com/deckarep/golang-set"
)
// DeduplicateNamespaces deduplicates a list of namespaces.
func DeduplicateNamespaces(namespaces ...Namespace) []Namespace {
nsSet := mapset.NewSet()
for _, ns := range namespaces {
nsSet.Add(ns)
}
uniqueNamespaces := make([]Namespace, 0, nsSet.Cardinality())
for ns := range nsSet.Iter() {
uniqueNamespaces = append(uniqueNamespaces, ns.(Namespace))
}
return uniqueNamespaces
}
// DeduplicateFeatures deduplicates a list of list of features.
func DeduplicateFeatures(features ...Feature) []Feature {
fSet := mapset.NewSet()
for _, f := range features {
fSet.Add(f)
}
return ConvertFeatureSetToFeatures(fSet)
}
// ConvertFeatureSetToFeatures converts a feature set to an array of features
func ConvertFeatureSetToFeatures(features mapset.Set) []Feature {
uniqueFeatures := make([]Feature, 0, features.Cardinality())
for f := range features.Iter() {
uniqueFeatures = append(uniqueFeatures, f.(Feature))
}
return uniqueFeatures
}
func ConvertFeatureSetToLayerFeatures(features mapset.Set) []LayerFeature {
uniqueLayerFeatures := make([]LayerFeature, 0, features.Cardinality())
for f := range features.Iter() {
feature := f.(Feature)
layerFeature := LayerFeature{
Feature: feature,
}
uniqueLayerFeatures = append(uniqueLayerFeatures, layerFeature)
}
return uniqueLayerFeatures
}
// FindKeyValueAndRollback wraps session FindKeyValue function with begin and
// roll back.
func FindKeyValueAndRollback(datastore Datastore, key string) (value string, ok bool, err error) {
var tx Session
tx, err = datastore.Begin()
if err != nil {
return
}
defer tx.Rollback()
value, ok, err = tx.FindKeyValue(key)
return
}
// PersistPartialLayerAndCommit wraps session PersistLayer function with begin and
// commit.
func PersistPartialLayerAndCommit(datastore Datastore, layer *Layer) error {
tx, err := datastore.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err := tx.PersistLayer(layer.Hash, layer.Features, layer.Namespaces, layer.By); err != nil {
return err
}
return tx.Commit()
}
// PersistFeaturesAndCommit wraps session PersistFeaturesAndCommit function with begin and commit.
func PersistFeaturesAndCommit(datastore Datastore, features []Feature) error {
tx, err := datastore.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err := tx.PersistFeatures(features); err != nil {
serialized, _ := json.Marshal(features)
log.WithError(err).WithField("feature", string(serialized)).Error("failed to store features")
return err
}
return tx.Commit()
}
// PersistNamespacesAndCommit wraps session PersistNamespaces function with
// begin and commit.
func PersistNamespacesAndCommit(datastore Datastore, namespaces []Namespace) error {
tx, err := datastore.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err := tx.PersistNamespaces(namespaces); err != nil {
return err
}
return tx.Commit()
}
// FindAncestryAndRollback wraps session FindAncestry function with begin and
// rollback.
func FindAncestryAndRollback(datastore Datastore, name string) (Ancestry, bool, error) {
tx, err := datastore.Begin()
defer tx.Rollback()
if err != nil {
return Ancestry{}, false, err
}
return tx.FindAncestry(name)
}
// FindLayerAndRollback wraps session FindLayer function with begin and rollback.
func FindLayerAndRollback(datastore Datastore, hash string) (layer *Layer, ok bool, err error) {
var tx Session
if tx, err = datastore.Begin(); err != nil {
return
}
defer tx.Rollback()
// TODO(sidac): In order to make the session interface more idiomatic, we'll
// return the pointer value in the future.
var dereferencedLayer Layer
dereferencedLayer, ok, err = tx.FindLayer(hash)
layer = &dereferencedLayer
return
}
// DeduplicateNamespacedFeatures returns a copy of all unique features in the
// input.
func DeduplicateNamespacedFeatures(features []NamespacedFeature) []NamespacedFeature {
nsSet := mapset.NewSet()
for _, ns := range features {
nsSet.Add(ns)
}
uniqueFeatures := make([]NamespacedFeature, 0, nsSet.Cardinality())
for ns := range nsSet.Iter() {
uniqueFeatures = append(uniqueFeatures, ns.(NamespacedFeature))
}
return uniqueFeatures
}
// GetAncestryFeatures returns a list of unique namespaced features in the
// ancestry.
func GetAncestryFeatures(ancestry Ancestry) []NamespacedFeature {
features := []NamespacedFeature{}
for _, layer := range ancestry.Layers {
features = append(features, layer.GetFeatures()...)
}
return DeduplicateNamespacedFeatures(features)
}
// UpsertAncestryAndCommit wraps session UpsertAncestry function with begin and commit.
func UpsertAncestryAndCommit(datastore Datastore, ancestry *Ancestry) error {
tx, err := datastore.Begin()
if err != nil {
return err
}
if err = tx.UpsertAncestry(*ancestry); err != nil {
log.WithError(err).Error("failed to upsert the ancestry")
serialized, _ := json.Marshal(ancestry)
log.Debug(string(serialized))
tx.Rollback()
return err
}
if err = tx.Commit(); err != nil {
return err
}
return nil
}
// PersistNamespacedFeaturesAndCommit wraps session PersistNamespacedFeatures function
// with begin and commit.
func PersistNamespacedFeaturesAndCommit(datastore Datastore, features []NamespacedFeature) error {
tx, err := datastore.Begin()
if err != nil {
return err
}
if err := tx.PersistNamespacedFeatures(features); err != nil {
tx.Rollback()
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
// CacheRelatedVulnerabilityAndCommit wraps session CacheAffectedNamespacedFeatures
// function with begin and commit.
func CacheRelatedVulnerabilityAndCommit(datastore Datastore, features []NamespacedFeature) error {
tx, err := datastore.Begin()
if err != nil {
return err
}
if err := tx.CacheAffectedNamespacedFeatures(features); err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
// IntersectDetectors returns the detectors in both d1 and d2.
func IntersectDetectors(d1 []Detector, d2 []Detector) []Detector {
d1Set := mapset.NewSet()
for _, d := range d1 {
d1Set.Add(d)
}
d2Set := mapset.NewSet()
for _, d := range d2 {
d2Set.Add(d)
}
inter := d1Set.Intersect(d2Set)
detectors := make([]Detector, 0, inter.Cardinality())
for d := range inter.Iter() {
detectors = append(detectors, d.(Detector))
}
return detectors
}
// DiffDetectors returns the detectors belongs to d1 but not d2
func DiffDetectors(d1 []Detector, d2 []Detector) []Detector {
d1Set := mapset.NewSet()
for _, d := range d1 {
d1Set.Add(d)
}
d2Set := mapset.NewSet()
for _, d := range d2 {
d2Set.Add(d)
}
diff := d1Set.Difference(d2Set)
detectors := make([]Detector, 0, diff.Cardinality())
for d := range diff.Iter() {
detectors = append(detectors, d.(Detector))
}
return detectors
}
// MergeLayers merges all content in new layer to l, where the content is
// updated.
func MergeLayers(l *Layer, new *Layer) *Layer {
featureSet := mapset.NewSet()
namespaceSet := mapset.NewSet()
bySet := mapset.NewSet()
for _, f := range l.Features {
featureSet.Add(f)
}
for _, ns := range l.Namespaces {
namespaceSet.Add(ns)
}
for _, d := range l.By {
bySet.Add(d)
}
for _, feature := range new.Features {
if !featureSet.Contains(feature) {
l.Features = append(l.Features, feature)
featureSet.Add(feature)
}
}
for _, namespace := range new.Namespaces {
if !namespaceSet.Contains(namespace) {
l.Namespaces = append(l.Namespaces, namespace)
namespaceSet.Add(namespace)
}
}
for _, detector := range new.By {
if !bySet.Contains(detector) {
l.By = append(l.By, detector)
bySet.Add(detector)
}
}
return l
}
// AcquireLock acquires a named global lock for a duration.
func AcquireLock(datastore Datastore, name, owner string, duration time.Duration) (acquired bool, expiration time.Time) {
tx, err := datastore.Begin()
if err != nil {
return false, time.Time{}
}
defer tx.Rollback()
locked, t, err := tx.AcquireLock(name, owner, duration)
if err != nil {
return false, time.Time{}
}
if locked {
if err := tx.Commit(); err != nil {
return false, time.Time{}
}
}
return locked, t
}
// ExtendLock extends the duration of an existing global lock for the given
// duration.
func ExtendLock(ds Datastore, name, whoami string, desiredLockDuration time.Duration) (bool, time.Time) {
tx, err := ds.Begin()
if err != nil {
return false, time.Time{}
}
defer tx.Rollback()
locked, expiration, err := tx.ExtendLock(name, whoami, desiredLockDuration)
if err != nil {
return false, time.Time{}
}
if locked {
if err := tx.Commit(); err == nil {
return locked, expiration
}
}
return false, time.Time{}
}
// ReleaseLock releases a named global lock.
func ReleaseLock(datastore Datastore, name, owner string) {
tx, err := datastore.Begin()
if err != nil {
return
}
defer tx.Rollback()
if err := tx.ReleaseLock(name, owner); err != nil {
return
}
if err := tx.Commit(); err != nil {
return
}
}
// PersistDetectorsAndCommit stores the detectors in the data store.
func PersistDetectorsAndCommit(store Datastore, detectors []Detector) error {
tx, err := store.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err := tx.PersistDetectors(detectors); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
// MarkNotificationAsReadAndCommit marks a notification as read.
func MarkNotificationAsReadAndCommit(store Datastore, name string) (bool, error) {
tx, err := store.Begin()
if err != nil {
return false, err
}
defer tx.Rollback()
err = tx.DeleteNotification(name)
if err == commonerr.ErrNotFound {
return false, nil
} else if err != nil {
return false, err
}
if err := tx.Commit(); err != nil {
return false, err
}
return true, nil
}
// FindAffectedNamespacedFeaturesAndRollback finds the vulnerabilities on each
// feature.
func FindAffectedNamespacedFeaturesAndRollback(store Datastore, features []NamespacedFeature) ([]NullableAffectedNamespacedFeature, error) {
tx, err := store.Begin()
if err != nil {
return nil, err
}
defer tx.Rollback()
nullableFeatures, err := tx.FindAffectedNamespacedFeatures(features)
if err != nil {
return nil, err
}
return nullableFeatures, nil
}
// FindVulnerabilityNotificationAndRollback finds the vulnerability notification
// and rollback.
func FindVulnerabilityNotificationAndRollback(store Datastore, name string, limit int, oldVulnerabilityPage pagination.Token, newVulnerabilityPage pagination.Token) (VulnerabilityNotificationWithVulnerable, bool, error) {
tx, err := store.Begin()
if err != nil {
return VulnerabilityNotificationWithVulnerable{}, false, err
}
defer tx.Rollback()
return tx.FindVulnerabilityNotification(name, limit, oldVulnerabilityPage, newVulnerabilityPage)
}
// FindNewNotification finds notifications either never notified or notified
// before the given time.
func FindNewNotification(store Datastore, notifiedBefore time.Time) (NotificationHook, bool, error) {
tx, err := store.Begin()
if err != nil {
return NotificationHook{}, false, err
}
defer tx.Rollback()
return tx.FindNewNotification(notifiedBefore)
}
// UpdateKeyValueAndCommit stores the key value to storage.
func UpdateKeyValueAndCommit(store Datastore, key, value string) error {
tx, err := store.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err = tx.UpdateKeyValue(key, value); err != nil {
return err
}
return tx.Commit()
}
// InsertVulnerabilityNotificationsAndCommit inserts the notifications into db
// and commit.
func InsertVulnerabilityNotificationsAndCommit(store Datastore, notifications []VulnerabilityNotification) error {
tx, err := store.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err := tx.InsertVulnerabilityNotifications(notifications); err != nil {
return err
}
return tx.Commit()
}
// FindVulnerabilitiesAndRollback finds the vulnerabilities based on given ids.
func FindVulnerabilitiesAndRollback(store Datastore, ids []VulnerabilityID) ([]NullableVulnerability, error) {
tx, err := store.Begin()
if err != nil {
return nil, err
}
defer tx.Rollback()
return tx.FindVulnerabilities(ids)
}
func UpdateVulnerabilitiesAndCommit(store Datastore, toRemove []VulnerabilityID, toAdd []VulnerabilityWithAffected) error {
tx, err := store.Begin()
if err != nil {
return err
}
if err := tx.DeleteVulnerabilities(toRemove); err != nil {
return err
}
if err := tx.InsertVulnerabilities(toAdd); err != nil {
return err
}
return tx.Commit()
}

@ -0,0 +1,144 @@
// Copyright 2018 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
import (
"database/sql/driver"
"errors"
"fmt"
"strings"
)
const (
// NamespaceDetectorType is a type of detector that extracts the namespaces.
NamespaceDetectorType DetectorType = "namespace"
// FeatureDetectorType is a type of detector that extracts the features.
FeatureDetectorType DetectorType = "feature"
)
// DetectorTypes contains all detector types.
var (
DetectorTypes = []DetectorType{
NamespaceDetectorType,
FeatureDetectorType,
}
// ErrFailedToParseDetectorType is the error returned when a detector type could
// not be parsed from a string.
ErrFailedToParseDetectorType = errors.New("failed to parse DetectorType from input")
// ErrInvalidDetector is the error returned when a detector from database has
// invalid name or version or type.
ErrInvalidDetector = errors.New("the detector has invalid metadata")
)
// DetectorType is the type of a detector.
type DetectorType string
// Value implements the database/sql/driver.Valuer interface.
func (s DetectorType) Value() (driver.Value, error) {
return string(s), nil
}
// Scan implements the database/sql.Scanner interface.
func (s *DetectorType) Scan(value interface{}) error {
val, ok := value.([]byte)
if !ok {
return errors.New("could not scan a Severity from a non-string input")
}
var err error
*s, err = NewDetectorType(string(val))
if err != nil {
return err
}
return nil
}
// NewDetectorType attempts to parse a string into a standard DetectorType
// value.
func NewDetectorType(s string) (DetectorType, error) {
for _, ss := range DetectorTypes {
if strings.EqualFold(s, string(ss)) {
return ss, nil
}
}
return "", ErrFailedToParseDetectorType
}
// Valid checks if a detector type is defined.
func (s DetectorType) Valid() bool {
for _, t := range DetectorTypes {
if s == t {
return true
}
}
return false
}
// Detector is an versioned Clair extension.
type Detector struct {
// Name of an extension should be non-empty and uniquely identifies the
// extension.
Name string `json:"name"`
// Version of an extension should be non-empty.
Version string `json:"version"`
// DType is the type of the extension and should be one of the types in
// DetectorTypes.
DType DetectorType `json:"dtype"`
}
// Valid checks if all fields in the detector satisfies the spec.
func (d Detector) Valid() bool {
if d.Name == "" || d.Version == "" || !d.DType.Valid() {
return false
}
return true
}
// String returns a unique string representation of the detector.
func (d Detector) String() string {
return fmt.Sprintf("%s:%s", d.Name, d.Version)
}
// NewNamespaceDetector returns a new namespace detector.
func NewNamespaceDetector(name, version string) Detector {
return Detector{
Name: name,
Version: version,
DType: NamespaceDetectorType,
}
}
// NewFeatureDetector returns a new feature detector.
func NewFeatureDetector(name, version string) Detector {
return Detector{
Name: name,
Version: version,
DType: FeatureDetectorType,
}
}
// SerializeDetectors returns the string representation of given detectors.
func SerializeDetectors(detectors []Detector) []string {
strDetectors := []string{}
for _, d := range detectors {
strDetectors = append(strDetectors, d.String())
}
return strDetectors
}

@ -0,0 +1,35 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
// StorageError is database error
type StorageError struct {
reason string
original error
}
func (e *StorageError) Error() string {
return e.reason
}
// NewStorageErrorWithInternalError creates a new database error
func NewStorageErrorWithInternalError(reason string, originalError error) *StorageError {
return &StorageError{reason, originalError}
}
// NewStorageError creates a new database error
func NewStorageError(reason string) *StorageError {
return &StorageError{reason, nil}
}

@ -0,0 +1,96 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
// Feature represents a package detected in a layer but the namespace is not
// determined.
//
// e.g. Name: Libssl1.0, Version: 1.0, VersionFormat: dpkg, Type: binary
// dpkg is the version format of the installer package manager, which in this
// case could be dpkg or apk.
type Feature struct {
Name string `json:"name"`
Version string `json:"version"`
VersionFormat string `json:"versionFormat"`
Type FeatureType `json:"type"`
}
// NamespacedFeature is a feature with determined namespace and can be affected
// by vulnerabilities.
//
// e.g. OpenSSL 1.0 dpkg Debian:7.
type NamespacedFeature struct {
Feature `json:"feature"`
Namespace Namespace `json:"namespace"`
}
// AffectedNamespacedFeature is a namespaced feature affected by the
// vulnerabilities with fixed-in versions for this feature.
type AffectedNamespacedFeature struct {
NamespacedFeature
AffectedBy []VulnerabilityWithFixedIn
}
// VulnerabilityWithFixedIn is used for AffectedNamespacedFeature to retrieve
// the affecting vulnerabilities and the fixed-in versions for the feature.
type VulnerabilityWithFixedIn struct {
Vulnerability
FixedInVersion string
}
// AffectedFeature is used to determine whether a namespaced feature is affected
// by a Vulnerability. Namespace and Feature Name is unique. Affected Feature is
// bound to vulnerability.
type AffectedFeature struct {
// FeatureType determines which type of package it affects.
FeatureType FeatureType
Namespace Namespace
FeatureName string
// FixedInVersion is known next feature version that's not affected by the
// vulnerability. Empty FixedInVersion means the unaffected version is
// unknown.
FixedInVersion string
// AffectedVersion contains the version range to determine whether or not a
// feature is affected.
AffectedVersion string
}
// NullableAffectedNamespacedFeature is an affectednamespacedfeature with
// whether it's found in datastore.
type NullableAffectedNamespacedFeature struct {
AffectedNamespacedFeature
Valid bool
}
func NewFeature(name string, version string, versionFormat string, featureType FeatureType) *Feature {
return &Feature{name, version, versionFormat, featureType}
}
func NewBinaryPackage(name string, version string, versionFormat string) *Feature {
return &Feature{name, version, versionFormat, BinaryPackage}
}
func NewSourcePackage(name string, version string, versionFormat string) *Feature {
return &Feature{name, version, versionFormat, SourcePackage}
}
func NewNamespacedFeature(namespace *Namespace, feature *Feature) *NamespacedFeature {
// TODO: namespaced feature should use pointer values
return &NamespacedFeature{*feature, *namespace}
}

@ -0,0 +1,52 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
import (
"database/sql/driver"
"fmt"
)
// FeatureType indicates the type of feature that a vulnerability
// affects.
type FeatureType string
const (
SourcePackage FeatureType = "source"
BinaryPackage FeatureType = "binary"
)
var featureTypes = []FeatureType{
SourcePackage,
BinaryPackage,
}
// Scan implements the database/sql.Scanner interface.
func (t *FeatureType) Scan(value interface{}) error {
val := value.(string)
for _, ft := range featureTypes {
if string(ft) == val {
*t = ft
return nil
}
}
panic(fmt.Sprintf("invalid feature type received from database: '%s'", val))
}
// Value implements the database/sql/driver.Valuer interface.
func (t *FeatureType) Value() (driver.Value, error) {
return string(*t), nil
}

@ -0,0 +1,65 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
// Layer is a layer with all the detected features and namespaces.
type Layer struct {
// Hash is the sha-256 tarsum on the layer's blob content.
Hash string `json:"hash"`
// By contains a list of detectors scanned this Layer.
By []Detector `json:"by"`
Namespaces []LayerNamespace `json:"namespaces"`
Features []LayerFeature `json:"features"`
}
func (l *Layer) GetFeatures() []Feature {
features := make([]Feature, 0, len(l.Features))
for _, f := range l.Features {
features = append(features, f.Feature)
}
return features
}
func (l *Layer) GetNamespaces() []Namespace {
namespaces := make([]Namespace, 0, len(l.Namespaces)+len(l.Features))
for _, ns := range l.Namespaces {
namespaces = append(namespaces, ns.Namespace)
}
for _, f := range l.Features {
if f.PotentialNamespace.Valid() {
namespaces = append(namespaces, f.PotentialNamespace)
}
}
return namespaces
}
// LayerNamespace is a namespace with detection information.
type LayerNamespace struct {
Namespace `json:"namespace"`
// By is the detector found the namespace.
By Detector `json:"by"`
}
// LayerFeature is a feature with detection information.
type LayerFeature struct {
Feature `json:"feature"`
// By is the detector found the feature.
By Detector `json:"by"`
PotentialNamespace Namespace `json:"potentialNamespace"`
}

@ -0,0 +1,41 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
import (
"database/sql/driver"
"encoding/json"
)
// MetadataMap is for storing the metadata returned by vulnerability database.
type MetadataMap map[string]interface{}
func (mm *MetadataMap) Scan(value interface{}) error {
if value == nil {
return nil
}
// github.com/lib/pq decodes TEXT/VARCHAR fields into strings.
val, ok := value.(string)
if !ok {
panic("got type other than []byte from database")
}
return json.Unmarshal([]byte(val), mm)
}
func (mm *MetadataMap) Value() (driver.Value, error) {
json, err := json.Marshal(*mm)
return string(json), err
}

@ -14,163 +14,230 @@
package database
import "time"
import (
"time"
// MockDatastore implements Datastore and enables overriding each available method.
"github.com/coreos/clair/pkg/pagination"
)
// MockSession implements Session and enables overriding each available method.
// The default behavior of each method is to simply panic.
type MockDatastore struct {
FctListNamespaces func() ([]Namespace, error)
FctInsertLayer func(Layer) error
FctFindLayer func(name string, withFeatures, withVulnerabilities bool) (Layer, error)
FctDeleteLayer func(name string) error
FctListVulnerabilities func(namespaceName string, limit int, page int) ([]Vulnerability, int, error)
FctInsertVulnerabilities func(vulnerabilities []Vulnerability, createNotification bool) error
FctFindVulnerability func(namespaceName, name string) (Vulnerability, error)
FctDeleteVulnerability func(namespaceName, name string) error
FctInsertVulnerabilityFixes func(vulnerabilityNamespace, vulnerabilityName string, fixes []FeatureVersion) error
FctDeleteVulnerabilityFix func(vulnerabilityNamespace, vulnerabilityName, featureName string) error
FctGetAvailableNotification func(renotifyInterval time.Duration) (VulnerabilityNotification, error)
FctGetNotification func(name string, limit int, page VulnerabilityNotificationPageNumber) (VulnerabilityNotification, VulnerabilityNotificationPageNumber, error)
FctSetNotificationNotified func(name string) error
FctDeleteNotification func(name string) error
FctInsertKeyValue func(key, value string) error
FctGetKeyValue func(key string) (string, error)
FctLock func(name string, owner string, duration time.Duration, renew bool) (bool, time.Time)
FctUnlock func(name, owner string)
FctFindLock func(name string) (string, time.Time, error)
FctPing func() bool
FctClose func()
type MockSession struct {
FctCommit func() error
FctRollback func() error
FctUpsertAncestry func(Ancestry) error
FctFindAncestry func(name string) (Ancestry, bool, error)
FctFindAffectedNamespacedFeatures func(features []NamespacedFeature) ([]NullableAffectedNamespacedFeature, error)
FctPersistNamespaces func([]Namespace) error
FctPersistFeatures func([]Feature) error
FctPersistDetectors func(detectors []Detector) error
FctPersistNamespacedFeatures func([]NamespacedFeature) error
FctCacheAffectedNamespacedFeatures func([]NamespacedFeature) error
FctPersistLayer func(hash string, features []LayerFeature, namespaces []LayerNamespace, by []Detector) error
FctFindLayer func(name string) (Layer, bool, error)
FctInsertVulnerabilities func([]VulnerabilityWithAffected) error
FctFindVulnerabilities func([]VulnerabilityID) ([]NullableVulnerability, error)
FctDeleteVulnerabilities func([]VulnerabilityID) error
FctInsertVulnerabilityNotifications func([]VulnerabilityNotification) error
FctFindNewNotification func(lastNotified time.Time) (NotificationHook, bool, error)
FctFindVulnerabilityNotification func(name string, limit int, oldPage pagination.Token, newPage pagination.Token) (
vuln VulnerabilityNotificationWithVulnerable, ok bool, err error)
FctMarkNotificationAsRead func(name string) error
FctDeleteNotification func(name string) error
FctUpdateKeyValue func(key, value string) error
FctFindKeyValue func(key string) (string, bool, error)
FctAcquireLock func(name, owner string, duration time.Duration) (bool, time.Time, error)
FctExtendLock func(name, owner string, duration time.Duration) (bool, time.Time, error)
FctReleaseLock func(name, owner string) error
}
func (mds *MockDatastore) ListNamespaces() ([]Namespace, error) {
if mds.FctListNamespaces != nil {
return mds.FctListNamespaces()
func (ms *MockSession) Commit() error {
if ms.FctCommit != nil {
return ms.FctCommit()
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) InsertLayer(layer Layer) error {
if mds.FctInsertLayer != nil {
return mds.FctInsertLayer(layer)
func (ms *MockSession) Rollback() error {
if ms.FctRollback != nil {
return ms.FctRollback()
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) FindLayer(name string, withFeatures, withVulnerabilities bool) (Layer, error) {
if mds.FctFindLayer != nil {
return mds.FctFindLayer(name, withFeatures, withVulnerabilities)
func (ms *MockSession) UpsertAncestry(ancestry Ancestry) error {
if ms.FctUpsertAncestry != nil {
return ms.FctUpsertAncestry(ancestry)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) DeleteLayer(name string) error {
if mds.FctDeleteLayer != nil {
return mds.FctDeleteLayer(name)
func (ms *MockSession) FindAncestry(name string) (Ancestry, bool, error) {
if ms.FctFindAncestry != nil {
return ms.FctFindAncestry(name)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) ListVulnerabilities(namespaceName string, limit int, page int) ([]Vulnerability, int, error) {
if mds.FctListVulnerabilities != nil {
return mds.FctListVulnerabilities(namespaceName, limit, page)
func (ms *MockSession) FindAffectedNamespacedFeatures(features []NamespacedFeature) ([]NullableAffectedNamespacedFeature, error) {
if ms.FctFindAffectedNamespacedFeatures != nil {
return ms.FctFindAffectedNamespacedFeatures(features)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) InsertVulnerabilities(vulnerabilities []Vulnerability, createNotification bool) error {
if mds.FctInsertVulnerabilities != nil {
return mds.FctInsertVulnerabilities(vulnerabilities, createNotification)
func (ms *MockSession) PersistDetectors(detectors []Detector) error {
if ms.FctPersistDetectors != nil {
return ms.FctPersistDetectors(detectors)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) FindVulnerability(namespaceName, name string) (Vulnerability, error) {
if mds.FctFindVulnerability != nil {
return mds.FctFindVulnerability(namespaceName, name)
func (ms *MockSession) PersistNamespaces(namespaces []Namespace) error {
if ms.FctPersistNamespaces != nil {
return ms.FctPersistNamespaces(namespaces)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) DeleteVulnerability(namespaceName, name string) error {
if mds.FctDeleteVulnerability != nil {
return mds.FctDeleteVulnerability(namespaceName, name)
func (ms *MockSession) PersistFeatures(features []Feature) error {
if ms.FctPersistFeatures != nil {
return ms.FctPersistFeatures(features)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) InsertVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName string, fixes []FeatureVersion) error {
if mds.FctInsertVulnerabilityFixes != nil {
return mds.FctInsertVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName, fixes)
func (ms *MockSession) PersistNamespacedFeatures(namespacedFeatures []NamespacedFeature) error {
if ms.FctPersistNamespacedFeatures != nil {
return ms.FctPersistNamespacedFeatures(namespacedFeatures)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) DeleteVulnerabilityFix(vulnerabilityNamespace, vulnerabilityName, featureName string) error {
if mds.FctDeleteVulnerabilityFix != nil {
return mds.FctDeleteVulnerabilityFix(vulnerabilityNamespace, vulnerabilityName, featureName)
func (ms *MockSession) CacheAffectedNamespacedFeatures(namespacedFeatures []NamespacedFeature) error {
if ms.FctCacheAffectedNamespacedFeatures != nil {
return ms.FctCacheAffectedNamespacedFeatures(namespacedFeatures)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) GetAvailableNotification(renotifyInterval time.Duration) (VulnerabilityNotification, error) {
if mds.FctGetAvailableNotification != nil {
return mds.FctGetAvailableNotification(renotifyInterval)
func (ms *MockSession) PersistLayer(hash string, features []LayerFeature, namespaces []LayerNamespace, detectors []Detector) error {
if ms.FctPersistLayer != nil {
return ms.FctPersistLayer(hash, features, namespaces, detectors)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) GetNotification(name string, limit int, page VulnerabilityNotificationPageNumber) (VulnerabilityNotification, VulnerabilityNotificationPageNumber, error) {
if mds.FctGetNotification != nil {
return mds.FctGetNotification(name, limit, page)
func (ms *MockSession) FindLayer(name string) (Layer, bool, error) {
if ms.FctFindLayer != nil {
return ms.FctFindLayer(name)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) SetNotificationNotified(name string) error {
if mds.FctSetNotificationNotified != nil {
return mds.FctSetNotificationNotified(name)
func (ms *MockSession) InsertVulnerabilities(vulnerabilities []VulnerabilityWithAffected) error {
if ms.FctInsertVulnerabilities != nil {
return ms.FctInsertVulnerabilities(vulnerabilities)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) DeleteNotification(name string) error {
if mds.FctDeleteNotification != nil {
return mds.FctDeleteNotification(name)
func (ms *MockSession) FindVulnerabilities(vulnerabilityIDs []VulnerabilityID) ([]NullableVulnerability, error) {
if ms.FctFindVulnerabilities != nil {
return ms.FctFindVulnerabilities(vulnerabilityIDs)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) InsertKeyValue(key, value string) error {
if mds.FctInsertKeyValue != nil {
return mds.FctInsertKeyValue(key, value)
func (ms *MockSession) DeleteVulnerabilities(VulnerabilityIDs []VulnerabilityID) error {
if ms.FctDeleteVulnerabilities != nil {
return ms.FctDeleteVulnerabilities(VulnerabilityIDs)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) GetKeyValue(key string) (string, error) {
if mds.FctGetKeyValue != nil {
return mds.FctGetKeyValue(key)
func (ms *MockSession) InsertVulnerabilityNotifications(vulnerabilityNotifications []VulnerabilityNotification) error {
if ms.FctInsertVulnerabilityNotifications != nil {
return ms.FctInsertVulnerabilityNotifications(vulnerabilityNotifications)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) Lock(name string, owner string, duration time.Duration, renew bool) (bool, time.Time) {
if mds.FctLock != nil {
return mds.FctLock(name, owner, duration, renew)
func (ms *MockSession) FindNewNotification(lastNotified time.Time) (NotificationHook, bool, error) {
if ms.FctFindNewNotification != nil {
return ms.FctFindNewNotification(lastNotified)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) Unlock(name, owner string) {
if mds.FctUnlock != nil {
mds.FctUnlock(name, owner)
return
func (ms *MockSession) FindVulnerabilityNotification(name string, limit int, oldPage pagination.Token, newPage pagination.Token) (
VulnerabilityNotificationWithVulnerable, bool, error) {
if ms.FctFindVulnerabilityNotification != nil {
return ms.FctFindVulnerabilityNotification(name, limit, oldPage, newPage)
}
panic("required mock function not implemented")
}
func (ms *MockSession) MarkNotificationAsRead(name string) error {
if ms.FctMarkNotificationAsRead != nil {
return ms.FctMarkNotificationAsRead(name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) DeleteNotification(name string) error {
if ms.FctDeleteNotification != nil {
return ms.FctDeleteNotification(name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) UpdateKeyValue(key, value string) error {
if ms.FctUpdateKeyValue != nil {
return ms.FctUpdateKeyValue(key, value)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindKeyValue(key string) (string, bool, error) {
if ms.FctFindKeyValue != nil {
return ms.FctFindKeyValue(key)
}
panic("required mock function not implemented")
}
func (ms *MockSession) AcquireLock(name, owner string, duration time.Duration) (bool, time.Time, error) {
if ms.FctAcquireLock != nil {
return ms.FctAcquireLock(name, owner, duration)
}
panic("required mock function not implemented")
}
func (ms *MockSession) ExtendLock(name, owner string, duration time.Duration) (bool, time.Time, error) {
if ms.FctExtendLock != nil {
return ms.FctExtendLock(name, owner, duration)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) FindLock(name string) (string, time.Time, error) {
if mds.FctFindLock != nil {
return mds.FctFindLock(name)
func (ms *MockSession) ReleaseLock(name, owner string) error {
if ms.FctReleaseLock != nil {
return ms.FctReleaseLock(name, owner)
}
panic("required mock function not implemented")
}
// MockDatastore implements Datastore and enables overriding each available method.
// The default behavior of each method is to simply panic.
type MockDatastore struct {
FctBegin func() (Session, error)
FctPing func() bool
FctClose func()
}
func (mds *MockDatastore) Begin() (Session, error) {
if mds.FctBegin != nil {
return mds.FctBegin()
}
panic("required mock function not implemented")
}

@ -1,123 +0,0 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
import (
"database/sql/driver"
"encoding/json"
"time"
)
// ID is only meant to be used by database implementations and should never be used for anything else.
type Model struct {
ID int
}
type Layer struct {
Model
Name string
EngineVersion int
Parent *Layer
Namespace *Namespace
Features []FeatureVersion
}
type Namespace struct {
Model
Name string
VersionFormat string
}
type Feature struct {
Model
Name string
Namespace Namespace
}
type FeatureVersion struct {
Model
Feature Feature
Version string
AffectedBy []Vulnerability
// For output purposes. Only make sense when the feature version is in the context of an image.
AddedBy Layer
}
type Vulnerability struct {
Model
Name string
Namespace Namespace
Description string
Link string
Severity Severity
Metadata MetadataMap
FixedIn []FeatureVersion
LayersIntroducingVulnerability []Layer
// For output purposes. Only make sense when the vulnerability
// is already about a specific Feature/FeatureVersion.
FixedBy string `json:",omitempty"`
}
type MetadataMap map[string]interface{}
func (mm *MetadataMap) Scan(value interface{}) error {
if value == nil {
return nil
}
// github.com/lib/pq decodes TEXT/VARCHAR fields into strings.
val, ok := value.(string)
if !ok {
panic("got type other than []byte from database")
}
return json.Unmarshal([]byte(val), mm)
}
func (mm *MetadataMap) Value() (driver.Value, error) {
json, err := json.Marshal(*mm)
return string(json), err
}
type VulnerabilityNotification struct {
Model
Name string
Created time.Time
Notified time.Time
Deleted time.Time
OldVulnerability *Vulnerability
NewVulnerability *Vulnerability
}
type VulnerabilityNotificationPageNumber struct {
// -1 means that we reached the end already.
OldVulnerability int
NewVulnerability int
}
var VulnerabilityNotificationFirstPage = VulnerabilityNotificationPageNumber{0, 0}
var NoVulnerabilityNotificationPage = VulnerabilityNotificationPageNumber{-1, -1}

@ -0,0 +1,34 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
// Namespace is the contextual information around features.
//
// e.g. Debian:7, NodeJS.
type Namespace struct {
Name string `json:"name"`
VersionFormat string `json:"versionFormat"`
}
func NewNamespace(name string, versionFormat string) *Namespace {
return &Namespace{name, versionFormat}
}
func (ns *Namespace) Valid() bool {
if ns.Name == "" || ns.VersionFormat == "" {
return false
}
return true
}

@ -46,4 +46,6 @@ var UbuntuReleasesMapping = map[string]string{
"zesty": "17.04",
"artful": "17.10",
"bionic": "18.04",
"cosmic": "18.10",
"disco": "19.04",
}

@ -0,0 +1,69 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
import (
"time"
"github.com/coreos/clair/pkg/pagination"
)
// NotificationHook is a message sent to another service to inform of a change
// to a Vulnerability or the Ancestries affected by a Vulnerability. It contains
// the name of a notification that should be read and marked as read via the
// API.
type NotificationHook struct {
Name string
Created time.Time
Notified time.Time
Deleted time.Time
}
// VulnerabilityNotification is a notification for vulnerability changes.
type VulnerabilityNotification struct {
NotificationHook
Old *Vulnerability
New *Vulnerability
}
// VulnerabilityNotificationWithVulnerable is a notification for vulnerability
// changes with vulnerable ancestries.
type VulnerabilityNotificationWithVulnerable struct {
NotificationHook
Old *PagedVulnerableAncestries
New *PagedVulnerableAncestries
}
// PagedVulnerableAncestries is a vulnerability with a page of affected
// ancestries each with a special index attached for streaming purpose. The
// current page number and next page number are for navigate.
type PagedVulnerableAncestries struct {
Vulnerability
// Affected is a map of special indexes to Ancestries, which the pair
// should be unique in a stream. Every indexes in the map should be larger
// than previous page.
Affected map[int]string
Limit int
Current pagination.Token
Next pagination.Token
// End signals the end of the pages.
End bool
}

@ -0,0 +1,160 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ancestry
import (
"database/sql"
"errors"
log "github.com/sirupsen/logrus"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/detector"
"github.com/coreos/clair/database/pgsql/layer"
"github.com/coreos/clair/database/pgsql/util"
)
const (
insertAncestry = `
INSERT INTO ancestry (name) VALUES ($1) RETURNING id`
findAncestryID = `SELECT id FROM ancestry WHERE name = $1`
removeAncestry = `DELETE FROM ancestry WHERE name = $1`
insertAncestryFeatures = `
INSERT INTO ancestry_feature
(ancestry_layer_id, namespaced_feature_id, feature_detector_id, namespace_detector_id) VALUES
($1, $2, $3, $4)`
)
func FindAncestry(tx *sql.Tx, name string) (database.Ancestry, bool, error) {
var (
ancestry = database.Ancestry{Name: name}
err error
)
id, ok, err := FindAncestryID(tx, name)
if !ok || err != nil {
return ancestry, ok, err
}
if ancestry.By, err = FindAncestryDetectors(tx, id); err != nil {
return ancestry, false, err
}
if ancestry.Layers, err = FindAncestryLayers(tx, id); err != nil {
return ancestry, false, err
}
return ancestry, true, nil
}
func UpsertAncestry(tx *sql.Tx, ancestry database.Ancestry) error {
if !ancestry.Valid() {
return database.ErrInvalidParameters
}
if err := RemoveAncestry(tx, ancestry.Name); err != nil {
return err
}
id, err := InsertAncestry(tx, ancestry.Name)
if err != nil {
return err
}
detectorIDs, err := detector.FindDetectorIDs(tx, ancestry.By)
if err != nil {
return err
}
// insert ancestry metadata
if err := InsertAncestryDetectors(tx, id, detectorIDs); err != nil {
return err
}
layers := make([]string, 0, len(ancestry.Layers))
for _, l := range ancestry.Layers {
layers = append(layers, l.Hash)
}
layerIDs, ok, err := layer.FindLayerIDs(tx, layers)
if err != nil {
return err
}
if !ok {
log.Error("layer cannot be found, this indicates that the internal logic of calling UpsertAncestry is wrong or the database is corrupted.")
return database.ErrMissingEntities
}
ancestryLayerIDs, err := InsertAncestryLayers(tx, id, layerIDs)
if err != nil {
return err
}
for i, id := range ancestryLayerIDs {
if err := InsertAncestryFeatures(tx, id, ancestry.Layers[i]); err != nil {
return err
}
}
return nil
}
func InsertAncestry(tx *sql.Tx, name string) (int64, error) {
var id int64
err := tx.QueryRow(insertAncestry, name).Scan(&id)
if err != nil {
if util.IsErrUniqueViolation(err) {
return 0, util.HandleError("insertAncestry", errors.New("other Go-routine is processing this ancestry (skip)"))
}
return 0, util.HandleError("insertAncestry", err)
}
return id, nil
}
func FindAncestryID(tx *sql.Tx, name string) (int64, bool, error) {
var id sql.NullInt64
if err := tx.QueryRow(findAncestryID, name).Scan(&id); err != nil {
if err == sql.ErrNoRows {
return 0, false, nil
}
return 0, false, util.HandleError("findAncestryID", err)
}
return id.Int64, true, nil
}
func RemoveAncestry(tx *sql.Tx, name string) error {
result, err := tx.Exec(removeAncestry, name)
if err != nil {
return util.HandleError("removeAncestry", err)
}
affected, err := result.RowsAffected()
if err != nil {
return util.HandleError("removeAncestry", err)
}
if affected != 0 {
log.WithField("ancestry", name).Debug("removed ancestry")
}
return nil
}

@ -0,0 +1,48 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ancestry
import (
"database/sql"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/detector"
"github.com/coreos/clair/database/pgsql/util"
)
var selectAncestryDetectors = `
SELECT d.name, d.version, d.dtype
FROM ancestry_detector, detector AS d
WHERE ancestry_detector.detector_id = d.id AND ancestry_detector.ancestry_id = $1;`
var insertAncestryDetectors = `
INSERT INTO ancestry_detector (ancestry_id, detector_id)
SELECT $1, $2
WHERE NOT EXISTS (SELECT id FROM ancestry_detector WHERE ancestry_id = $1 AND detector_id = $2)`
func FindAncestryDetectors(tx *sql.Tx, id int64) ([]database.Detector, error) {
detectors, err := detector.GetDetectors(tx, selectAncestryDetectors, id)
return detectors, err
}
func InsertAncestryDetectors(tx *sql.Tx, ancestryID int64, detectorIDs []int64) error {
for _, detectorID := range detectorIDs {
if _, err := tx.Exec(insertAncestryDetectors, ancestryID, detectorID); err != nil {
return util.HandleError("insertAncestryDetectors", err)
}
}
return nil
}

@ -0,0 +1,146 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ancestry
import (
"database/sql"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/detector"
"github.com/coreos/clair/database/pgsql/feature"
"github.com/coreos/clair/database/pgsql/util"
"github.com/coreos/clair/pkg/commonerr"
)
const findAncestryFeatures = `
SELECT namespace.name, namespace.version_format, feature.name,
feature.version, feature.version_format, feature_type.name, ancestry_layer.ancestry_index,
ancestry_feature.feature_detector_id, ancestry_feature.namespace_detector_id
FROM namespace, feature, feature_type, namespaced_feature, ancestry_layer, ancestry_feature
WHERE ancestry_layer.ancestry_id = $1
AND feature_type.id = feature.type
AND ancestry_feature.ancestry_layer_id = ancestry_layer.id
AND ancestry_feature.namespaced_feature_id = namespaced_feature.id
AND namespaced_feature.feature_id = feature.id
AND namespaced_feature.namespace_id = namespace.id`
func FindAncestryFeatures(tx *sql.Tx, ancestryID int64, detectors detector.DetectorMap) (map[int64][]database.AncestryFeature, error) {
// ancestry_index -> ancestry features
featureMap := make(map[int64][]database.AncestryFeature)
// retrieve ancestry layer's namespaced features
rows, err := tx.Query(findAncestryFeatures, ancestryID)
if err != nil {
return nil, util.HandleError("findAncestryFeatures", err)
}
defer rows.Close()
for rows.Next() {
var (
featureDetectorID int64
namespaceDetectorID sql.NullInt64
feature database.NamespacedFeature
// index is used to determine which layer the feature belongs to.
index sql.NullInt64
)
if err := rows.Scan(
&feature.Namespace.Name,
&feature.Namespace.VersionFormat,
&feature.Feature.Name,
&feature.Feature.Version,
&feature.Feature.VersionFormat,
&feature.Feature.Type,
&index,
&featureDetectorID,
&namespaceDetectorID,
); err != nil {
return nil, util.HandleError("findAncestryFeatures", err)
}
if feature.Feature.VersionFormat != feature.Namespace.VersionFormat {
// Feature must have the same version format as the associated
// namespace version format.
return nil, database.ErrInconsistent
}
fDetector, ok := detectors.ByID[featureDetectorID]
if !ok {
return nil, database.ErrInconsistent
}
var nsDetector database.Detector
if !namespaceDetectorID.Valid {
nsDetector = database.Detector{}
} else {
nsDetector, ok = detectors.ByID[namespaceDetectorID.Int64]
if !ok {
return nil, database.ErrInconsistent
}
}
featureMap[index.Int64] = append(featureMap[index.Int64], database.AncestryFeature{
NamespacedFeature: feature,
FeatureBy: fDetector,
NamespaceBy: nsDetector,
})
}
return featureMap, nil
}
func InsertAncestryFeatures(tx *sql.Tx, ancestryLayerID int64, layer database.AncestryLayer) error {
detectors, err := detector.FindAllDetectors(tx)
if err != nil {
return err
}
nsFeatureIDs, err := feature.FindNamespacedFeatureIDs(tx, layer.GetFeatures())
if err != nil {
return err
}
// find the detectors for each feature
stmt, err := tx.Prepare(insertAncestryFeatures)
if err != nil {
return util.HandleError("insertAncestryFeatures", err)
}
defer stmt.Close()
for index, id := range nsFeatureIDs {
if !id.Valid {
return database.ErrMissingEntities
}
var namespaceDetectorID sql.NullInt64
var ok bool
namespaceDetectorID.Int64, ok = detectors.ByValue[layer.Features[index].NamespaceBy]
if ok {
namespaceDetectorID.Valid = true
}
featureDetectorID, ok := detectors.ByValue[layer.Features[index].FeatureBy]
if !ok {
return database.ErrMissingEntities
}
if _, err := stmt.Exec(ancestryLayerID, id, featureDetectorID, namespaceDetectorID); err != nil {
return util.HandleError("insertAncestryFeatures", commonerr.CombineErrors(err, stmt.Close()))
}
}
return nil
}

@ -0,0 +1,131 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ancestry
import (
"database/sql"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/detector"
"github.com/coreos/clair/database/pgsql/util"
"github.com/coreos/clair/pkg/commonerr"
log "github.com/sirupsen/logrus"
)
const (
findAncestryLayerHashes = `
SELECT layer.hash, ancestry_layer.ancestry_index
FROM layer, ancestry_layer
WHERE ancestry_layer.ancestry_id = $1
AND ancestry_layer.layer_id = layer.id
ORDER BY ancestry_layer.ancestry_index ASC`
insertAncestryLayers = `
INSERT INTO ancestry_layer (ancestry_id, ancestry_index, layer_id) VALUES ($1, $2, $3)
RETURNING id`
)
func FindAncestryLayerHashes(tx *sql.Tx, ancestryID int64) (map[int64]string, error) {
// retrieve layer indexes and hashes
rows, err := tx.Query(findAncestryLayerHashes, ancestryID)
if err != nil {
return nil, util.HandleError("findAncestryLayerHashes", err)
}
layerHashes := map[int64]string{}
for rows.Next() {
var (
hash string
index int64
)
if err = rows.Scan(&hash, &index); err != nil {
return nil, util.HandleError("findAncestryLayerHashes", err)
}
if _, ok := layerHashes[index]; ok {
// one ancestry index should correspond to only one layer
return nil, database.ErrInconsistent
}
layerHashes[index] = hash
}
return layerHashes, nil
}
// insertAncestryLayers inserts the ancestry layers along with its content into
// the database. The layers are 0 based indexed in the original order.
func InsertAncestryLayers(tx *sql.Tx, ancestryID int64, layers []int64) ([]int64, error) {
stmt, err := tx.Prepare(insertAncestryLayers)
if err != nil {
return nil, util.HandleError("insertAncestryLayers", err)
}
ancestryLayerIDs := []int64{}
for index, layerID := range layers {
var ancestryLayerID sql.NullInt64
if err := stmt.QueryRow(ancestryID, index, layerID).Scan(&ancestryLayerID); err != nil {
return nil, util.HandleError("insertAncestryLayers", commonerr.CombineErrors(err, stmt.Close()))
}
if !ancestryLayerID.Valid {
return nil, database.ErrInconsistent
}
ancestryLayerIDs = append(ancestryLayerIDs, ancestryLayerID.Int64)
}
if err := stmt.Close(); err != nil {
return nil, util.HandleError("insertAncestryLayers", err)
}
return ancestryLayerIDs, nil
}
func FindAncestryLayers(tx *sql.Tx, id int64) ([]database.AncestryLayer, error) {
detectors, err := detector.FindAllDetectors(tx)
if err != nil {
return nil, err
}
layerMap, err := FindAncestryLayerHashes(tx, id)
if err != nil {
return nil, err
}
featureMap, err := FindAncestryFeatures(tx, id, detectors)
if err != nil {
return nil, err
}
layers := make([]database.AncestryLayer, len(layerMap))
for index, layer := range layerMap {
// index MUST match the ancestry layer slice index.
if layers[index].Hash == "" && len(layers[index].Features) == 0 {
layers[index] = database.AncestryLayer{
Hash: layer,
Features: featureMap[index],
}
} else {
log.WithFields(log.Fields{
"ancestry ID": id,
"duplicated ancestry index": index,
}).WithError(database.ErrInconsistent).Error("ancestry layers with same ancestry_index is not allowed")
return nil, database.ErrInconsistent
}
}
return layers, nil
}

@ -0,0 +1,141 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ancestry
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/testutil"
)
var upsertAncestryTests = []struct {
in *database.Ancestry
err string
title string
}{
{
title: "ancestry with invalid layer",
in: &database.Ancestry{
Name: "a1",
Layers: []database.AncestryLayer{
{
Hash: "layer-non-existing",
},
},
},
err: database.ErrMissingEntities.Error(),
},
{
title: "ancestry with invalid name",
in: &database.Ancestry{},
err: database.ErrInvalidParameters.Error(),
},
{
title: "new valid ancestry",
in: &database.Ancestry{
Name: "a",
Layers: []database.AncestryLayer{{Hash: "layer-0"}},
},
},
{
title: "ancestry with invalid feature",
in: &database.Ancestry{
Name: "a",
By: []database.Detector{testutil.RealDetectors[1], testutil.RealDetectors[2]},
Layers: []database.AncestryLayer{{Hash: "layer-1", Features: []database.AncestryFeature{
{testutil.FakeNamespacedFeatures[1], testutil.FakeDetector[1], testutil.FakeDetector[2]},
}}},
},
err: database.ErrMissingEntities.Error(),
},
{
title: "replace old ancestry",
in: &database.Ancestry{
Name: "a",
By: []database.Detector{testutil.RealDetectors[1], testutil.RealDetectors[2]},
Layers: []database.AncestryLayer{
{"layer-1", []database.AncestryFeature{{testutil.RealNamespacedFeatures[1], testutil.RealDetectors[2], testutil.RealDetectors[1]}}},
},
},
},
}
func TestUpsertAncestry(t *testing.T) {
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "TestUpsertAncestry")
defer cleanup()
for _, test := range upsertAncestryTests {
t.Run(test.title, func(t *testing.T) {
err := UpsertAncestry(tx, *test.in)
if test.err != "" {
assert.EqualError(t, err, test.err, "unexpected error")
return
}
assert.Nil(t, err)
actual, ok, err := FindAncestry(tx, test.in.Name)
assert.Nil(t, err)
assert.True(t, ok)
database.AssertAncestryEqual(t, test.in, &actual)
})
}
}
var findAncestryTests = []struct {
title string
in string
ancestry *database.Ancestry
err string
ok bool
}{
{
title: "missing ancestry",
in: "ancestry-non",
err: "",
ancestry: nil,
ok: false,
},
{
title: "valid ancestry",
in: "ancestry-2",
err: "",
ok: true,
ancestry: testutil.TakeAncestryPointerFromMap(testutil.RealAncestries, 2),
},
}
func TestFindAncestry(t *testing.T) {
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "TestFindAncestry")
defer cleanup()
for _, test := range findAncestryTests {
t.Run(test.title, func(t *testing.T) {
ancestry, ok, err := FindAncestry(tx, test.in)
if test.err != "" {
assert.EqualError(t, err, test.err, "unexpected error")
return
}
assert.Nil(t, err)
assert.Equal(t, test.ok, ok)
if test.ok {
database.AssertAncestryEqual(t, test.ancestry, &ancestry)
}
})
}
}

@ -1,161 +0,0 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pgsql
import (
"fmt"
"math/rand"
"runtime"
"strconv"
"sync"
"testing"
"time"
"github.com/pborman/uuid"
"github.com/stretchr/testify/assert"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt/dpkg"
)
const (
numVulnerabilities = 100
numFeatureVersions = 100
)
func TestRaceAffects(t *testing.T) {
datastore, err := openDatabaseForTest("RaceAffects", false)
if err != nil {
t.Error(err)
return
}
defer datastore.Close()
// Insert the Feature on which we'll work.
feature := database.Feature{
Namespace: database.Namespace{
Name: "TestRaceAffectsFeatureNamespace1",
VersionFormat: dpkg.ParserName,
},
Name: "TestRaceAffecturesFeature1",
}
_, err = datastore.insertFeature(feature)
if err != nil {
t.Error(err)
return
}
// Initialize random generator and enforce max procs.
rand.Seed(time.Now().UnixNano())
runtime.GOMAXPROCS(runtime.NumCPU())
// Generate FeatureVersions.
featureVersions := make([]database.FeatureVersion, numFeatureVersions)
for i := 0; i < numFeatureVersions; i++ {
version := rand.Intn(numFeatureVersions)
featureVersions[i] = database.FeatureVersion{
Feature: feature,
Version: strconv.Itoa(version),
}
}
// Generate vulnerabilities.
// They are mapped by fixed version, which will make verification really easy afterwards.
vulnerabilities := make(map[int][]database.Vulnerability)
for i := 0; i < numVulnerabilities; i++ {
version := rand.Intn(numFeatureVersions) + 1
// if _, ok := vulnerabilities[version]; !ok {
// vulnerabilities[version] = make([]database.Vulnerability)
// }
vulnerability := database.Vulnerability{
Name: uuid.New(),
Namespace: feature.Namespace,
FixedIn: []database.FeatureVersion{
{
Feature: feature,
Version: strconv.Itoa(version),
},
},
Severity: database.UnknownSeverity,
}
vulnerabilities[version] = append(vulnerabilities[version], vulnerability)
}
// Insert featureversions and vulnerabilities in parallel.
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for _, vulnerabilitiesM := range vulnerabilities {
for _, vulnerability := range vulnerabilitiesM {
err = datastore.InsertVulnerabilities([]database.Vulnerability{vulnerability}, true)
assert.Nil(t, err)
}
}
fmt.Println("finished to insert vulnerabilities")
}()
go func() {
defer wg.Done()
for i := 0; i < len(featureVersions); i++ {
featureVersions[i].ID, err = datastore.insertFeatureVersion(featureVersions[i])
assert.Nil(t, err)
}
fmt.Println("finished to insert featureVersions")
}()
wg.Wait()
// Verify consistency now.
var actualAffectedNames []string
var expectedAffectedNames []string
for _, featureVersion := range featureVersions {
featureVersionVersion, _ := strconv.Atoi(featureVersion.Version)
// Get actual affects.
rows, err := datastore.Query(searchComplexTestFeatureVersionAffects,
featureVersion.ID)
assert.Nil(t, err)
defer rows.Close()
var vulnName string
for rows.Next() {
err = rows.Scan(&vulnName)
if !assert.Nil(t, err) {
continue
}
actualAffectedNames = append(actualAffectedNames, vulnName)
}
if assert.Nil(t, rows.Err()) {
rows.Close()
}
// Get expected affects.
for i := numVulnerabilities; i > featureVersionVersion; i-- {
for _, vulnerability := range vulnerabilities[i] {
expectedAffectedNames = append(expectedAffectedNames, vulnerability.Name)
}
}
assert.Len(t, compareStringLists(expectedAffectedNames, actualAffectedNames), 0)
assert.Len(t, compareStringLists(actualAffectedNames, expectedAffectedNames), 0)
}
}

@ -0,0 +1,132 @@
// Copyright 2018 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package detector
import (
"database/sql"
log "github.com/sirupsen/logrus"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/util"
)
const (
soiDetector = `
INSERT INTO detector (name, version, dtype)
SELECT CAST ($1 AS TEXT), CAST ($2 AS TEXT), CAST ($3 AS detector_type )
WHERE NOT EXISTS (SELECT id FROM detector WHERE name = $1 AND version = $2 AND dtype = $3);`
findDetectorID = `SELECT id FROM detector WHERE name = $1 AND version = $2 AND dtype = $3`
findAllDetectors = `SELECT id, name, version, dtype FROM detector`
)
type DetectorMap struct {
ByID map[int64]database.Detector
ByValue map[database.Detector]int64
}
func PersistDetectors(tx *sql.Tx, detectors []database.Detector) error {
for _, d := range detectors {
if !d.Valid() {
log.WithField("detector", d).Debug("Invalid Detector")
return database.ErrInvalidParameters
}
r, err := tx.Exec(soiDetector, d.Name, d.Version, d.DType)
if err != nil {
return util.HandleError("soiDetector", err)
}
count, err := r.RowsAffected()
if err != nil {
return util.HandleError("soiDetector", err)
}
if count == 0 {
log.Debug("detector already exists: ", d)
}
}
return nil
}
// findDetectorIDs retrieve ids of the detectors from the database, if any is not
// found, return the error.
func FindDetectorIDs(tx *sql.Tx, detectors []database.Detector) ([]int64, error) {
ids := []int64{}
for _, d := range detectors {
id := sql.NullInt64{}
err := tx.QueryRow(findDetectorID, d.Name, d.Version, d.DType).Scan(&id)
if err != nil {
return nil, util.HandleError("findDetectorID", err)
}
if !id.Valid {
return nil, database.ErrInconsistent
}
ids = append(ids, id.Int64)
}
return ids, nil
}
func GetDetectors(tx *sql.Tx, query string, id int64) ([]database.Detector, error) {
rows, err := tx.Query(query, id)
if err != nil {
return nil, util.HandleError("getDetectors", err)
}
detectors := []database.Detector{}
for rows.Next() {
d := database.Detector{}
err := rows.Scan(&d.Name, &d.Version, &d.DType)
if err != nil {
return nil, util.HandleError("getDetectors", err)
}
if !d.Valid() {
return nil, database.ErrInvalidDetector
}
detectors = append(detectors, d)
}
return detectors, nil
}
func FindAllDetectors(tx *sql.Tx) (DetectorMap, error) {
rows, err := tx.Query(findAllDetectors)
if err != nil {
return DetectorMap{}, util.HandleError("searchAllDetectors", err)
}
detectors := DetectorMap{ByID: make(map[int64]database.Detector), ByValue: make(map[database.Detector]int64)}
for rows.Next() {
var (
id int64
d database.Detector
)
if err := rows.Scan(&id, &d.Name, &d.Version, &d.DType); err != nil {
return DetectorMap{}, util.HandleError("searchAllDetectors", err)
}
detectors.ByID[id] = d
detectors.ByValue[d] = id
}
return detectors, nil
}

@ -0,0 +1,121 @@
// Copyright 2018 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package detector
import (
"database/sql"
"testing"
"github.com/deckarep/golang-set"
"github.com/stretchr/testify/require"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/testutil"
)
func testGetAllDetectors(tx *sql.Tx) []database.Detector {
query := `SELECT name, version, dtype FROM detector`
rows, err := tx.Query(query)
if err != nil {
panic(err)
}
detectors := []database.Detector{}
for rows.Next() {
d := database.Detector{}
if err := rows.Scan(&d.Name, &d.Version, &d.DType); err != nil {
panic(err)
}
detectors = append(detectors, d)
}
return detectors
}
var persistDetectorTests = []struct {
title string
in []database.Detector
err string
}{
{
title: "invalid detector",
in: []database.Detector{
{},
database.NewFeatureDetector("name", "2.0"),
},
err: database.ErrInvalidParameters.Error(),
},
{
title: "invalid detector 2",
in: []database.Detector{
database.NewFeatureDetector("name", "2.0"),
{"name", "1.0", "random not valid dtype"},
},
err: database.ErrInvalidParameters.Error(),
},
{
title: "detectors with some different fields",
in: []database.Detector{
database.NewFeatureDetector("name", "2.0"),
database.NewFeatureDetector("name", "1.0"),
database.NewNamespaceDetector("name", "1.0"),
},
},
{
title: "duplicated detectors (parameter level)",
in: []database.Detector{
database.NewFeatureDetector("name", "1.0"),
database.NewFeatureDetector("name", "1.0"),
},
},
{
title: "duplicated detectors (db level)",
in: []database.Detector{
database.NewNamespaceDetector("os-release", "1.0"),
database.NewNamespaceDetector("os-release", "1.0"),
database.NewFeatureDetector("dpkg", "1.0"),
},
},
}
func TestPersistDetector(t *testing.T) {
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "PersistDetector")
defer cleanup()
for _, test := range persistDetectorTests {
t.Run(test.title, func(t *testing.T) {
err := PersistDetectors(tx, test.in)
if test.err != "" {
require.EqualError(t, err, test.err)
return
}
detectors := testGetAllDetectors(tx)
// ensure no duplicated detectors
detectorSet := mapset.NewSet()
for _, d := range detectors {
require.False(t, detectorSet.Contains(d), "duplicated: %v", d)
detectorSet.Add(d)
}
// ensure all persisted detectors are actually saved
for _, d := range test.in {
require.True(t, detectorSet.Contains(d), "detector: %v, detectors: %v", d, detectorSet)
}
})
}
}

@ -1,245 +0,0 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pgsql
import (
"database/sql"
"strings"
"time"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/pkg/commonerr"
)
func (pgSQL *pgSQL) insertFeature(feature database.Feature) (int, error) {
if feature.Name == "" {
return 0, commonerr.NewBadRequestError("could not find/insert invalid Feature")
}
// Do cache lookup.
if pgSQL.cache != nil {
promCacheQueriesTotal.WithLabelValues("feature").Inc()
id, found := pgSQL.cache.Get("feature:" + feature.Namespace.Name + ":" + feature.Name)
if found {
promCacheHitsTotal.WithLabelValues("feature").Inc()
return id.(int), nil
}
}
// We do `defer observeQueryTime` here because we don't want to observe cached features.
defer observeQueryTime("insertFeature", "all", time.Now())
// Find or create Namespace.
namespaceID, err := pgSQL.insertNamespace(feature.Namespace)
if err != nil {
return 0, err
}
// Find or create Feature.
var id int
err = pgSQL.QueryRow(soiFeature, feature.Name, namespaceID).Scan(&id)
if err != nil {
return 0, handleError("soiFeature", err)
}
if pgSQL.cache != nil {
pgSQL.cache.Add("feature:"+feature.Namespace.Name+":"+feature.Name, id)
}
return id, nil
}
func (pgSQL *pgSQL) insertFeatureVersion(fv database.FeatureVersion) (id int, err error) {
err = versionfmt.Valid(fv.Feature.Namespace.VersionFormat, fv.Version)
if err != nil {
return 0, commonerr.NewBadRequestError("could not find/insert invalid FeatureVersion")
}
// Do cache lookup.
cacheIndex := strings.Join([]string{"featureversion", fv.Feature.Namespace.Name, fv.Feature.Name, fv.Version}, ":")
if pgSQL.cache != nil {
promCacheQueriesTotal.WithLabelValues("featureversion").Inc()
id, found := pgSQL.cache.Get(cacheIndex)
if found {
promCacheHitsTotal.WithLabelValues("featureversion").Inc()
return id.(int), nil
}
}
// We do `defer observeQueryTime` here because we don't want to observe cached featureversions.
defer observeQueryTime("insertFeatureVersion", "all", time.Now())
// Find or create Feature first.
t := time.Now()
featureID, err := pgSQL.insertFeature(fv.Feature)
observeQueryTime("insertFeatureVersion", "insertFeature", t)
if err != nil {
return 0, err
}
fv.Feature.ID = featureID
// Try to find the FeatureVersion.
//
// In a populated database, the likelihood of the FeatureVersion already being there is high.
// If we can find it here, we then avoid using a transaction and locking the database.
err = pgSQL.QueryRow(searchFeatureVersion, featureID, fv.Version).Scan(&fv.ID)
if err != nil && err != sql.ErrNoRows {
return 0, handleError("searchFeatureVersion", err)
}
if err == nil {
if pgSQL.cache != nil {
pgSQL.cache.Add(cacheIndex, fv.ID)
}
return fv.ID, nil
}
// Begin transaction.
tx, err := pgSQL.Begin()
if err != nil {
tx.Rollback()
return 0, handleError("insertFeatureVersion.Begin()", err)
}
// Lock Vulnerability_Affects_FeatureVersion exclusively.
// We want to prevent InsertVulnerability to modify it.
promConcurrentLockVAFV.Inc()
defer promConcurrentLockVAFV.Dec()
t = time.Now()
_, err = tx.Exec(lockVulnerabilityAffects)
observeQueryTime("insertFeatureVersion", "lock", t)
if err != nil {
tx.Rollback()
return 0, handleError("insertFeatureVersion.lockVulnerabilityAffects", err)
}
// Find or create FeatureVersion.
var created bool
t = time.Now()
err = tx.QueryRow(soiFeatureVersion, featureID, fv.Version).Scan(&created, &fv.ID)
observeQueryTime("insertFeatureVersion", "soiFeatureVersion", t)
if err != nil {
tx.Rollback()
return 0, handleError("soiFeatureVersion", err)
}
if !created {
// The featureVersion already existed, no need to link it to
// vulnerabilities.
tx.Commit()
if pgSQL.cache != nil {
pgSQL.cache.Add(cacheIndex, fv.ID)
}
return fv.ID, nil
}
// Link the new FeatureVersion with every vulnerabilities that affect it, by inserting in
// Vulnerability_Affects_FeatureVersion.
t = time.Now()
err = linkFeatureVersionToVulnerabilities(tx, fv)
observeQueryTime("insertFeatureVersion", "linkFeatureVersionToVulnerabilities", t)
if err != nil {
tx.Rollback()
return 0, err
}
// Commit transaction.
err = tx.Commit()
if err != nil {
return 0, handleError("insertFeatureVersion.Commit()", err)
}
if pgSQL.cache != nil {
pgSQL.cache.Add(cacheIndex, fv.ID)
}
return fv.ID, nil
}
// TODO(Quentin-M): Batch me
func (pgSQL *pgSQL) insertFeatureVersions(featureVersions []database.FeatureVersion) ([]int, error) {
IDs := make([]int, 0, len(featureVersions))
for i := 0; i < len(featureVersions); i++ {
id, err := pgSQL.insertFeatureVersion(featureVersions[i])
if err != nil {
return IDs, err
}
IDs = append(IDs, id)
}
return IDs, nil
}
type vulnerabilityAffectsFeatureVersion struct {
vulnerabilityID int
fixedInID int
fixedInVersion string
}
func linkFeatureVersionToVulnerabilities(tx *sql.Tx, featureVersion database.FeatureVersion) error {
// Select every vulnerability and the fixed version that affect this Feature.
// TODO(Quentin-M): LIMIT
rows, err := tx.Query(searchVulnerabilityFixedInFeature, featureVersion.Feature.ID)
if err != nil {
return handleError("searchVulnerabilityFixedInFeature", err)
}
defer rows.Close()
var affects []vulnerabilityAffectsFeatureVersion
for rows.Next() {
var affect vulnerabilityAffectsFeatureVersion
err := rows.Scan(&affect.fixedInID, &affect.vulnerabilityID, &affect.fixedInVersion)
if err != nil {
return handleError("searchVulnerabilityFixedInFeature.Scan()", err)
}
cmp, err := versionfmt.Compare(featureVersion.Feature.Namespace.VersionFormat, featureVersion.Version, affect.fixedInVersion)
if err != nil {
return err
}
if cmp < 0 {
// The version of the FeatureVersion we are inserting is lower than the fixed version on this
// Vulnerability, thus, this FeatureVersion is affected by it.
affects = append(affects, affect)
}
}
if err = rows.Err(); err != nil {
return handleError("searchVulnerabilityFixedInFeature.Rows()", err)
}
rows.Close()
// Insert into Vulnerability_Affects_FeatureVersion.
for _, affect := range affects {
// TODO(Quentin-M): Batch me.
_, err := tx.Exec(insertVulnerabilityAffectsFeatureVersion, affect.vulnerabilityID,
featureVersion.ID, affect.fixedInID)
if err != nil {
return handleError("insertVulnerabilityAffectsFeatureVersion", err)
}
}
return nil
}

@ -0,0 +1,121 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package feature
import (
"database/sql"
"fmt"
"sort"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/util"
"github.com/coreos/clair/pkg/commonerr"
)
func queryPersistFeature(count int) string {
return util.QueryPersist(count,
"feature",
"feature_name_version_version_format_type_key",
"name",
"version",
"version_format",
"type")
}
func querySearchFeatureID(featureCount int) string {
return fmt.Sprintf(`
SELECT id, name, version, version_format, type
FROM Feature WHERE (name, version, version_format, type) IN (%s)`,
util.QueryString(4, featureCount),
)
}
func PersistFeatures(tx *sql.Tx, features []database.Feature) error {
if len(features) == 0 {
return nil
}
types, err := GetFeatureTypeMap(tx)
if err != nil {
return err
}
// Sorting is needed before inserting into database to prevent deadlock.
sort.Slice(features, func(i, j int) bool {
return features[i].Name < features[j].Name ||
features[i].Version < features[j].Version ||
features[i].VersionFormat < features[j].VersionFormat
})
// TODO(Sida): A better interface for bulk insertion is needed.
keys := make([]interface{}, 0, len(features)*3)
for _, f := range features {
keys = append(keys, f.Name, f.Version, f.VersionFormat, types.ByName[f.Type])
if f.Name == "" || f.Version == "" || f.VersionFormat == "" {
return commonerr.NewBadRequestError("Empty feature name, version or version format is not allowed")
}
}
_, err = tx.Exec(queryPersistFeature(len(features)), keys...)
return util.HandleError("queryPersistFeature", err)
}
func FindFeatureIDs(tx *sql.Tx, fs []database.Feature) ([]sql.NullInt64, error) {
if len(fs) == 0 {
return nil, nil
}
types, err := GetFeatureTypeMap(tx)
if err != nil {
return nil, err
}
fMap := map[database.Feature]sql.NullInt64{}
keys := make([]interface{}, 0, len(fs)*4)
for _, f := range fs {
typeID := types.ByName[f.Type]
keys = append(keys, f.Name, f.Version, f.VersionFormat, typeID)
fMap[f] = sql.NullInt64{}
}
rows, err := tx.Query(querySearchFeatureID(len(fs)), keys...)
if err != nil {
return nil, util.HandleError("querySearchFeatureID", err)
}
defer rows.Close()
var (
id sql.NullInt64
f database.Feature
)
for rows.Next() {
var typeID int
err := rows.Scan(&id, &f.Name, &f.Version, &f.VersionFormat, &typeID)
if err != nil {
return nil, util.HandleError("querySearchFeatureID", err)
}
f.Type = types.ByID[typeID]
fMap[f] = id
}
ids := make([]sql.NullInt64, len(fs))
for i, f := range fs {
ids[i] = fMap[f]
}
return ids, nil
}

@ -0,0 +1,154 @@
// Copyright 2016 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package feature
import (
"database/sql"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/testutil"
)
func TestPersistFeatures(t *testing.T) {
tx, cleanup := testutil.CreateTestTx(t, "TestPersistFeatures")
defer cleanup()
invalid := database.Feature{}
valid := *database.NewBinaryPackage("mount", "2.31.1-0.4ubuntu3.1", "dpkg")
// invalid
require.NotNil(t, PersistFeatures(tx, []database.Feature{invalid}))
// existing
require.Nil(t, PersistFeatures(tx, []database.Feature{valid}))
require.Nil(t, PersistFeatures(tx, []database.Feature{valid}))
features := selectAllFeatures(t, tx)
assert.Equal(t, []database.Feature{valid}, features)
}
func TestPersistNamespacedFeatures(t *testing.T) {
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "TestPersistNamespacedFeatures")
defer cleanup()
// existing features
f1 := database.NewSourcePackage("ourchat", "0.5", "dpkg")
// non-existing features
f2 := database.NewSourcePackage("fake!", "", "")
// exising namespace
n1 := database.NewNamespace("debian:7", "dpkg")
// non-existing namespace
n2 := database.NewNamespace("debian:non", "dpkg")
// existing namespaced feature
nf1 := database.NewNamespacedFeature(n1, f1)
// invalid namespaced feature
nf2 := database.NewNamespacedFeature(n2, f2)
// namespaced features with namespaces or features not in the database will
// generate error.
assert.Nil(t, PersistNamespacedFeatures(tx, []database.NamespacedFeature{}))
assert.NotNil(t, PersistNamespacedFeatures(tx, []database.NamespacedFeature{*nf1, *nf2}))
// valid case: insert nf3
assert.Nil(t, PersistNamespacedFeatures(tx, []database.NamespacedFeature{*nf1}))
all := listNamespacedFeatures(t, tx)
assert.Contains(t, all, *nf1)
}
func listNamespacedFeatures(t *testing.T, tx *sql.Tx) []database.NamespacedFeature {
types, err := GetFeatureTypeMap(tx)
if err != nil {
panic(err)
}
rows, err := tx.Query(`SELECT f.name, f.version, f.version_format, f.type, n.name, n.version_format
FROM feature AS f, namespace AS n, namespaced_feature AS nf
WHERE nf.feature_id = f.id AND nf.namespace_id = n.id`)
if err != nil {
panic(err)
}
nf := []database.NamespacedFeature{}
for rows.Next() {
f := database.NamespacedFeature{}
var typeID int
err := rows.Scan(&f.Name, &f.Version, &f.VersionFormat, &typeID, &f.Namespace.Name, &f.Namespace.VersionFormat)
if err != nil {
panic(err)
}
f.Type = types.ByID[typeID]
nf = append(nf, f)
}
return nf
}
func selectAllFeatures(t *testing.T, tx *sql.Tx) []database.Feature {
types, err := GetFeatureTypeMap(tx)
if err != nil {
panic(err)
}
rows, err := tx.Query("SELECT name, version, version_format, type FROM feature")
if err != nil {
t.FailNow()
}
fs := []database.Feature{}
for rows.Next() {
f := database.Feature{}
var typeID int
err := rows.Scan(&f.Name, &f.Version, &f.VersionFormat, &typeID)
f.Type = types.ByID[typeID]
if err != nil {
t.FailNow()
}
fs = append(fs, f)
}
return fs
}
func TestFindNamespacedFeatureIDs(t *testing.T) {
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "TestFindNamespacedFeatureIDs")
defer cleanup()
features := []database.NamespacedFeature{}
expectedIDs := []int{}
for id, feature := range testutil.RealNamespacedFeatures {
features = append(features, feature)
expectedIDs = append(expectedIDs, id)
}
features = append(features, testutil.RealNamespacedFeatures[1]) // test duplicated
expectedIDs = append(expectedIDs, 1)
namespace := testutil.RealNamespaces[1]
features = append(features, *database.NewNamespacedFeature(&namespace, database.NewBinaryPackage("not-found", "1.0", "dpkg"))) // test not found feature
ids, err := FindNamespacedFeatureIDs(tx, features)
require.Nil(t, err)
require.Len(t, ids, len(expectedIDs)+1)
for i, id := range ids {
if i == len(ids)-1 {
require.False(t, id.Valid)
} else {
require.True(t, id.Valid)
require.Equal(t, expectedIDs[i], int(id.Int64))
}
}
}

@ -0,0 +1,57 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package feature
import (
"database/sql"
"github.com/coreos/clair/database"
)
const (
selectAllFeatureTypes = `SELECT id, name FROM feature_type`
)
type FeatureTypes struct {
ByID map[int]database.FeatureType
ByName map[database.FeatureType]int
}
func newFeatureTypes() *FeatureTypes {
return &FeatureTypes{make(map[int]database.FeatureType), make(map[database.FeatureType]int)}
}
func GetFeatureTypeMap(tx *sql.Tx) (*FeatureTypes, error) {
rows, err := tx.Query(selectAllFeatureTypes)
if err != nil {
return nil, err
}
types := newFeatureTypes()
for rows.Next() {
var (
id int
name database.FeatureType
)
if err := rows.Scan(&id, &name); err != nil {
return nil, err
}
types.ByID[id] = name
types.ByName[name] = id
}
return types, nil
}

@ -0,0 +1,39 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package feature
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/testutil"
)
func TestGetFeatureTypeMap(t *testing.T) {
tx, cleanup := testutil.CreateTestTx(t, "TestGetFeatureTypeMap")
defer cleanup()
types, err := GetFeatureTypeMap(tx)
if err != nil {
require.Nil(t, err, err.Error())
}
require.Equal(t, database.SourcePackage, types.ByID[1])
require.Equal(t, database.BinaryPackage, types.ByID[2])
require.Equal(t, 1, types.ByName[database.SourcePackage])
require.Equal(t, 2, types.ByName[database.BinaryPackage])
}

@ -0,0 +1,168 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package feature
import (
"database/sql"
"fmt"
"sort"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/namespace"
"github.com/coreos/clair/database/pgsql/util"
)
var soiNamespacedFeature = `
WITH new_feature_ns AS (
INSERT INTO namespaced_feature(feature_id, namespace_id)
SELECT CAST ($1 AS INTEGER), CAST ($2 AS INTEGER)
WHERE NOT EXISTS ( SELECT id FROM namespaced_feature WHERE namespaced_feature.feature_id = $1 AND namespaced_feature.namespace_id = $2)
RETURNING id
)
SELECT id FROM namespaced_feature WHERE namespaced_feature.feature_id = $1 AND namespaced_feature.namespace_id = $2
UNION
SELECT id FROM new_feature_ns`
func queryPersistNamespacedFeature(count int) string {
return util.QueryPersist(count, "namespaced_feature",
"namespaced_feature_namespace_id_feature_id_key",
"feature_id",
"namespace_id")
}
func querySearchNamespacedFeature(nsfCount int) string {
return fmt.Sprintf(`
SELECT nf.id, f.name, f.version, f.version_format, t.name, n.name
FROM namespaced_feature AS nf, feature AS f, namespace AS n, feature_type AS t
WHERE nf.feature_id = f.id
AND nf.namespace_id = n.id
AND n.version_format = f.version_format
AND f.type = t.id
AND (f.name, f.version, f.version_format, t.name, n.name) IN (%s)`,
util.QueryString(5, nsfCount),
)
}
type namespacedFeatureWithID struct {
database.NamespacedFeature
ID int64
}
func PersistNamespacedFeatures(tx *sql.Tx, features []database.NamespacedFeature) error {
if len(features) == 0 {
return nil
}
nsIDs := map[database.Namespace]sql.NullInt64{}
fIDs := map[database.Feature]sql.NullInt64{}
for _, f := range features {
nsIDs[f.Namespace] = sql.NullInt64{}
fIDs[f.Feature] = sql.NullInt64{}
}
fToFind := []database.Feature{}
for f := range fIDs {
fToFind = append(fToFind, f)
}
sort.Slice(fToFind, func(i, j int) bool {
return fToFind[i].Name < fToFind[j].Name ||
fToFind[i].Version < fToFind[j].Version ||
fToFind[i].VersionFormat < fToFind[j].VersionFormat
})
if ids, err := FindFeatureIDs(tx, fToFind); err == nil {
for i, id := range ids {
if !id.Valid {
return database.ErrMissingEntities
}
fIDs[fToFind[i]] = id
}
} else {
return err
}
nsToFind := []database.Namespace{}
for ns := range nsIDs {
nsToFind = append(nsToFind, ns)
}
if ids, err := namespace.FindNamespaceIDs(tx, nsToFind); err == nil {
for i, id := range ids {
if !id.Valid {
return database.ErrMissingEntities
}
nsIDs[nsToFind[i]] = id
}
} else {
return err
}
keys := make([]interface{}, 0, len(features)*2)
for _, f := range features {
keys = append(keys, fIDs[f.Feature], nsIDs[f.Namespace])
}
_, err := tx.Exec(queryPersistNamespacedFeature(len(features)), keys...)
if err != nil {
return err
}
return nil
}
func FindNamespacedFeatureIDs(tx *sql.Tx, nfs []database.NamespacedFeature) ([]sql.NullInt64, error) {
if len(nfs) == 0 {
return nil, nil
}
nfsMap := map[database.NamespacedFeature]int64{}
keys := make([]interface{}, 0, len(nfs)*5)
for _, nf := range nfs {
keys = append(keys, nf.Name, nf.Version, nf.VersionFormat, nf.Type, nf.Namespace.Name)
}
rows, err := tx.Query(querySearchNamespacedFeature(len(nfs)), keys...)
if err != nil {
return nil, util.HandleError("searchNamespacedFeature", err)
}
defer rows.Close()
var (
id int64
nf database.NamespacedFeature
)
for rows.Next() {
err := rows.Scan(&id, &nf.Name, &nf.Version, &nf.VersionFormat, &nf.Type, &nf.Namespace.Name)
nf.Namespace.VersionFormat = nf.VersionFormat
if err != nil {
return nil, util.HandleError("searchNamespacedFeature", err)
}
nfsMap[nf] = id
}
ids := make([]sql.NullInt64, len(nfs))
for i, nf := range nfs {
if id, ok := nfsMap[nf]; ok {
ids[i] = sql.NullInt64{id, true}
} else {
ids[i] = sql.NullInt64{}
}
}
return ids, nil
}

@ -1,115 +0,0 @@
// Copyright 2016 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pgsql
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt/dpkg"
)
func TestInsertFeature(t *testing.T) {
datastore, err := openDatabaseForTest("InsertFeature", false)
if err != nil {
t.Error(err)
return
}
defer datastore.Close()
// Invalid Feature.
id0, err := datastore.insertFeature(database.Feature{})
assert.NotNil(t, err)
assert.Zero(t, id0)
id0, err = datastore.insertFeature(database.Feature{
Namespace: database.Namespace{},
Name: "TestInsertFeature0",
})
assert.NotNil(t, err)
assert.Zero(t, id0)
// Insert Feature and ensure we can find it.
feature := database.Feature{
Namespace: database.Namespace{
Name: "TestInsertFeatureNamespace1",
VersionFormat: dpkg.ParserName,
},
Name: "TestInsertFeature1",
}
id1, err := datastore.insertFeature(feature)
assert.Nil(t, err)
id2, err := datastore.insertFeature(feature)
assert.Nil(t, err)
assert.Equal(t, id1, id2)
// Insert invalid FeatureVersion.
for _, invalidFeatureVersion := range []database.FeatureVersion{
{
Feature: database.Feature{},
Version: "1.0",
},
{
Feature: database.Feature{
Namespace: database.Namespace{},
Name: "TestInsertFeature2",
},
Version: "1.0",
},
{
Feature: database.Feature{
Namespace: database.Namespace{
Name: "TestInsertFeatureNamespace2",
VersionFormat: dpkg.ParserName,
},
Name: "TestInsertFeature2",
},
Version: "",
},
{
Feature: database.Feature{
Namespace: database.Namespace{
Name: "TestInsertFeatureNamespace2",
VersionFormat: dpkg.ParserName,
},
Name: "TestInsertFeature2",
},
Version: "bad version",
},
} {
id3, err := datastore.insertFeatureVersion(invalidFeatureVersion)
assert.Error(t, err)
assert.Zero(t, id3)
}
// Insert FeatureVersion and ensure we can find it.
featureVersion := database.FeatureVersion{
Feature: database.Feature{
Namespace: database.Namespace{
Name: "TestInsertFeatureNamespace1",
VersionFormat: dpkg.ParserName,
},
Name: "TestInsertFeature1",
},
Version: "2:3.0-imba",
}
id4, err := datastore.insertFeatureVersion(featureVersion)
assert.Nil(t, err)
id5, err := datastore.insertFeatureVersion(featureVersion)
assert.Nil(t, err)
assert.Equal(t, id4, id5)
}

@ -1,85 +0,0 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pgsql
import (
"database/sql"
"time"
log "github.com/sirupsen/logrus"
"github.com/coreos/clair/pkg/commonerr"
)
// InsertKeyValue stores (or updates) a single key / value tuple.
func (pgSQL *pgSQL) InsertKeyValue(key, value string) (err error) {
if key == "" || value == "" {
log.Warning("could not insert a flag which has an empty name or value")
return commonerr.NewBadRequestError("could not insert a flag which has an empty name or value")
}
defer observeQueryTime("InsertKeyValue", "all", time.Now())
// Upsert.
//
// Note: UPSERT works only on >= PostgreSQL 9.5 which is not yet supported by AWS RDS.
// The best solution is currently the use of http://dba.stackexchange.com/a/13477
// but the key/value storage doesn't need to be super-efficient and super-safe at the
// moment so we can just use a client-side solution with transactions, based on
// http://postgresql.org/docs/current/static/plpgsql-control-structures.html.
// TODO(Quentin-M): Enable Upsert as soon as 9.5 is stable.
for {
// First, try to update.
r, err := pgSQL.Exec(updateKeyValue, value, key)
if err != nil {
return handleError("updateKeyValue", err)
}
if n, _ := r.RowsAffected(); n > 0 {
// Updated successfully.
return nil
}
// Try to insert the key.
// If someone else inserts the same key concurrently, we could get a unique-key violation error.
_, err = pgSQL.Exec(insertKeyValue, key, value)
if err != nil {
if isErrUniqueViolation(err) {
// Got unique constraint violation, retry.
continue
}
return handleError("insertKeyValue", err)
}
return nil
}
}
// GetValue reads a single key / value tuple and returns an empty string if the key doesn't exist.
func (pgSQL *pgSQL) GetKeyValue(key string) (string, error) {
defer observeQueryTime("GetKeyValue", "all", time.Now())
var value string
err := pgSQL.QueryRow(searchKeyValue, key).Scan(&value)
if err == sql.ErrNoRows {
return "", nil
}
if err != nil {
return "", handleError("searchKeyValue", err)
}
return value, nil
}

@ -0,0 +1,68 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package keyvalue
import (
"database/sql"
"time"
log "github.com/sirupsen/logrus"
"github.com/coreos/clair/database/pgsql/monitoring"
"github.com/coreos/clair/database/pgsql/util"
"github.com/coreos/clair/pkg/commonerr"
)
const (
searchKeyValue = `SELECT value FROM KeyValue WHERE key = $1`
upsertKeyValue = `
INSERT INTO KeyValue(key, value)
VALUES ($1, $2)
ON CONFLICT ON CONSTRAINT keyvalue_key_key
DO UPDATE SET key=$1, value=$2`
)
func UpdateKeyValue(tx *sql.Tx, key, value string) (err error) {
if key == "" || value == "" {
log.Warning("could not insert a flag which has an empty name or value")
return commonerr.NewBadRequestError("could not insert a flag which has an empty name or value")
}
defer monitoring.ObserveQueryTime("PersistKeyValue", "all", time.Now())
_, err = tx.Exec(upsertKeyValue, key, value)
if err != nil {
return util.HandleError("insertKeyValue", err)
}
return nil
}
func FindKeyValue(tx *sql.Tx, key string) (string, bool, error) {
defer monitoring.ObserveQueryTime("FindKeyValue", "all", time.Now())
var value string
err := tx.QueryRow(searchKeyValue, key).Scan(&value)
if err == sql.ErrNoRows {
return "", false, nil
}
if err != nil {
return "", false, util.HandleError("searchKeyValue", err)
}
return value, true, nil
}

@ -12,41 +12,40 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package pgsql
package keyvalue
import (
"testing"
"github.com/coreos/clair/database/pgsql/testutil"
"github.com/stretchr/testify/assert"
)
func TestKeyValue(t *testing.T) {
datastore, err := openDatabaseForTest("KeyValue", false)
if err != nil {
t.Error(err)
return
}
defer datastore.Close()
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "KeyValue")
defer cleanup()
// Get non-existing key/value
f, err := datastore.GetKeyValue("test")
f, ok, err := FindKeyValue(tx, "test")
assert.Nil(t, err)
assert.Empty(t, "", f)
assert.False(t, ok)
// Try to insert invalid key/value.
assert.Error(t, datastore.InsertKeyValue("test", ""))
assert.Error(t, datastore.InsertKeyValue("", "test"))
assert.Error(t, datastore.InsertKeyValue("", ""))
assert.Error(t, UpdateKeyValue(tx, "test", ""))
assert.Error(t, UpdateKeyValue(tx, "", "test"))
assert.Error(t, UpdateKeyValue(tx, "", ""))
// Insert and verify.
assert.Nil(t, datastore.InsertKeyValue("test", "test1"))
f, err = datastore.GetKeyValue("test")
assert.Nil(t, UpdateKeyValue(tx, "test", "test1"))
f, ok, err = FindKeyValue(tx, "test")
assert.Nil(t, err)
assert.True(t, ok)
assert.Equal(t, "test1", f)
// Update and verify.
assert.Nil(t, datastore.InsertKeyValue("test", "test2"))
f, err = datastore.GetKeyValue("test")
assert.Nil(t, UpdateKeyValue(tx, "test", "test2"))
f, ok, err = FindKeyValue(tx, "test")
assert.Nil(t, err)
assert.True(t, ok)
assert.Equal(t, "test2", f)
}

@ -1,436 +0,0 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pgsql
import (
"database/sql"
"strings"
"time"
"github.com/guregu/null/zero"
log "github.com/sirupsen/logrus"
"github.com/coreos/clair/database"
"github.com/coreos/clair/pkg/commonerr"
)
func (pgSQL *pgSQL) FindLayer(name string, withFeatures, withVulnerabilities bool) (database.Layer, error) {
subquery := "all"
if withFeatures {
subquery += "/features"
} else if withVulnerabilities {
subquery += "/features+vulnerabilities"
}
defer observeQueryTime("FindLayer", subquery, time.Now())
// Find the layer
var (
layer database.Layer
parentID zero.Int
parentName zero.String
nsID zero.Int
nsName sql.NullString
nsVersionFormat sql.NullString
)
t := time.Now()
err := pgSQL.QueryRow(searchLayer, name).Scan(
&layer.ID,
&layer.Name,
&layer.EngineVersion,
&parentID,
&parentName,
&nsID,
&nsName,
&nsVersionFormat,
)
observeQueryTime("FindLayer", "searchLayer", t)
if err != nil {
return layer, handleError("searchLayer", err)
}
if !parentID.IsZero() {
layer.Parent = &database.Layer{
Model: database.Model{ID: int(parentID.Int64)},
Name: parentName.String,
}
}
if !nsID.IsZero() {
layer.Namespace = &database.Namespace{
Model: database.Model{ID: int(nsID.Int64)},
Name: nsName.String,
VersionFormat: nsVersionFormat.String,
}
}
// Find its features
if withFeatures || withVulnerabilities {
// Create a transaction to disable hash/merge joins as our experiments have shown that
// PostgreSQL 9.4 makes bad planning decisions about:
// - joining the layer tree to feature versions and feature
// - joining the feature versions to affected/fixed feature version and vulnerabilities
// It would for instance do a merge join between affected feature versions (300 rows, estimated
// 3000 rows) and fixed in feature version (100k rows). In this case, it is much more
// preferred to use a nested loop.
tx, err := pgSQL.Begin()
if err != nil {
return layer, handleError("FindLayer.Begin()", err)
}
defer tx.Commit()
_, err = tx.Exec(disableHashJoin)
if err != nil {
log.WithError(err).Warningf("FindLayer: could not disable hash join")
}
_, err = tx.Exec(disableMergeJoin)
if err != nil {
log.WithError(err).Warningf("FindLayer: could not disable merge join")
}
t = time.Now()
featureVersions, err := getLayerFeatureVersions(tx, layer.ID)
observeQueryTime("FindLayer", "getLayerFeatureVersions", t)
if err != nil {
return layer, err
}
layer.Features = featureVersions
if withVulnerabilities {
// Load the vulnerabilities that affect the FeatureVersions.
t = time.Now()
err := loadAffectedBy(tx, layer.Features)
observeQueryTime("FindLayer", "loadAffectedBy", t)
if err != nil {
return layer, err
}
}
}
return layer, nil
}
// getLayerFeatureVersions returns list of database.FeatureVersion that a database.Layer has.
func getLayerFeatureVersions(tx *sql.Tx, layerID int) ([]database.FeatureVersion, error) {
var featureVersions []database.FeatureVersion
// Query.
rows, err := tx.Query(searchLayerFeatureVersion, layerID)
if err != nil {
return featureVersions, handleError("searchLayerFeatureVersion", err)
}
defer rows.Close()
// Scan query.
var modification string
mapFeatureVersions := make(map[int]database.FeatureVersion)
for rows.Next() {
var fv database.FeatureVersion
err = rows.Scan(
&fv.ID,
&modification,
&fv.Feature.Namespace.ID,
&fv.Feature.Namespace.Name,
&fv.Feature.Namespace.VersionFormat,
&fv.Feature.ID,
&fv.Feature.Name,
&fv.ID,
&fv.Version,
&fv.AddedBy.ID,
&fv.AddedBy.Name,
)
if err != nil {
return featureVersions, handleError("searchLayerFeatureVersion.Scan()", err)
}
// Do transitive closure.
switch modification {
case "add":
mapFeatureVersions[fv.ID] = fv
case "del":
delete(mapFeatureVersions, fv.ID)
default:
log.WithField("modification", modification).Warning("unknown Layer_diff_FeatureVersion's modification")
return featureVersions, database.ErrInconsistent
}
}
if err = rows.Err(); err != nil {
return featureVersions, handleError("searchLayerFeatureVersion.Rows()", err)
}
// Build result by converting our map to a slice.
for _, featureVersion := range mapFeatureVersions {
featureVersions = append(featureVersions, featureVersion)
}
return featureVersions, nil
}
// loadAffectedBy returns the list of database.Vulnerability that affect the given
// FeatureVersion.
func loadAffectedBy(tx *sql.Tx, featureVersions []database.FeatureVersion) error {
if len(featureVersions) == 0 {
return nil
}
// Construct list of FeatureVersion IDs, we will do a single query
featureVersionIDs := make([]int, 0, len(featureVersions))
for i := 0; i < len(featureVersions); i++ {
featureVersionIDs = append(featureVersionIDs, featureVersions[i].ID)
}
rows, err := tx.Query(searchFeatureVersionVulnerability,
buildInputArray(featureVersionIDs))
if err != nil && err != sql.ErrNoRows {
return handleError("searchFeatureVersionVulnerability", err)
}
defer rows.Close()
vulnerabilities := make(map[int][]database.Vulnerability, len(featureVersions))
var featureversionID int
for rows.Next() {
var vulnerability database.Vulnerability
err := rows.Scan(
&featureversionID,
&vulnerability.ID,
&vulnerability.Name,
&vulnerability.Description,
&vulnerability.Link,
&vulnerability.Severity,
&vulnerability.Metadata,
&vulnerability.Namespace.Name,
&vulnerability.Namespace.VersionFormat,
&vulnerability.FixedBy,
)
if err != nil {
return handleError("searchFeatureVersionVulnerability.Scan()", err)
}
vulnerabilities[featureversionID] = append(vulnerabilities[featureversionID], vulnerability)
}
if err = rows.Err(); err != nil {
return handleError("searchFeatureVersionVulnerability.Rows()", err)
}
// Assign vulnerabilities to every FeatureVersions
for i := 0; i < len(featureVersions); i++ {
featureVersions[i].AffectedBy = vulnerabilities[featureVersions[i].ID]
}
return nil
}
// Internally, only Feature additions/removals are stored for each layer. If a layer has a parent,
// the Feature list will be compared to the parent's Feature list and the difference will be stored.
// Note that when the Namespace of a layer differs from its parent, it is expected that several
// Feature that were already included a parent will have their Namespace updated as well
// (happens when Feature detectors relies on the detected layer Namespace). However, if the listed
// Feature has the same Name/Version as its parent, InsertLayer considers that the Feature hasn't
// been modified.
func (pgSQL *pgSQL) InsertLayer(layer database.Layer) error {
tf := time.Now()
// Verify parameters
if layer.Name == "" {
log.Warning("could not insert a layer which has an empty Name")
return commonerr.NewBadRequestError("could not insert a layer which has an empty Name")
}
// Get a potentially existing layer.
existingLayer, err := pgSQL.FindLayer(layer.Name, true, false)
if err != nil && err != commonerr.ErrNotFound {
return err
} else if err == nil {
if existingLayer.EngineVersion >= layer.EngineVersion {
// The layer exists and has an equal or higher engine version, do nothing.
return nil
}
layer.ID = existingLayer.ID
}
// We do `defer observeQueryTime` here because we don't want to observe existing layers.
defer observeQueryTime("InsertLayer", "all", tf)
// Get parent ID.
var parentID zero.Int
if layer.Parent != nil {
if layer.Parent.ID == 0 {
log.Warning("Parent is expected to be retrieved from database when inserting a layer.")
return commonerr.NewBadRequestError("Parent is expected to be retrieved from database when inserting a layer.")
}
parentID = zero.IntFrom(int64(layer.Parent.ID))
}
// Find or insert namespace if provided.
var namespaceID zero.Int
if layer.Namespace != nil {
n, err := pgSQL.insertNamespace(*layer.Namespace)
if err != nil {
return err
}
namespaceID = zero.IntFrom(int64(n))
} else if layer.Namespace == nil && layer.Parent != nil {
// Import the Namespace from the parent if it has one and this layer doesn't specify one.
if layer.Parent.Namespace != nil {
namespaceID = zero.IntFrom(int64(layer.Parent.Namespace.ID))
}
}
// Begin transaction.
tx, err := pgSQL.Begin()
if err != nil {
tx.Rollback()
return handleError("InsertLayer.Begin()", err)
}
if layer.ID == 0 {
// Insert a new layer.
err = tx.QueryRow(insertLayer, layer.Name, layer.EngineVersion, parentID, namespaceID).
Scan(&layer.ID)
if err != nil {
tx.Rollback()
if isErrUniqueViolation(err) {
// Ignore this error, another process collided.
log.Debug("Attempted to insert duplicate layer.")
return nil
}
return handleError("insertLayer", err)
}
} else {
// Update an existing layer.
_, err = tx.Exec(updateLayer, layer.ID, layer.EngineVersion, namespaceID)
if err != nil {
tx.Rollback()
return handleError("updateLayer", err)
}
// Remove all existing Layer_diff_FeatureVersion.
_, err = tx.Exec(removeLayerDiffFeatureVersion, layer.ID)
if err != nil {
tx.Rollback()
return handleError("removeLayerDiffFeatureVersion", err)
}
}
// Update Layer_diff_FeatureVersion now.
err = pgSQL.updateDiffFeatureVersions(tx, &layer, &existingLayer)
if err != nil {
tx.Rollback()
return err
}
// Commit transaction.
err = tx.Commit()
if err != nil {
tx.Rollback()
return handleError("InsertLayer.Commit()", err)
}
return nil
}
func (pgSQL *pgSQL) updateDiffFeatureVersions(tx *sql.Tx, layer, existingLayer *database.Layer) error {
// add and del are the FeatureVersion diff we should insert.
var add []database.FeatureVersion
var del []database.FeatureVersion
if layer.Parent == nil {
// There is no parent, every Features are added.
add = append(add, layer.Features...)
} else if layer.Parent != nil {
// There is a parent, we need to diff the Features with it.
// Build name:version structures.
layerFeaturesMapNV, layerFeaturesNV := createNV(layer.Features)
parentLayerFeaturesMapNV, parentLayerFeaturesNV := createNV(layer.Parent.Features)
// Calculate the added and deleted FeatureVersions name:version.
addNV := compareStringLists(layerFeaturesNV, parentLayerFeaturesNV)
delNV := compareStringLists(parentLayerFeaturesNV, layerFeaturesNV)
// Fill the structures containing the added and deleted FeatureVersions.
for _, nv := range addNV {
add = append(add, *layerFeaturesMapNV[nv])
}
for _, nv := range delNV {
del = append(del, *parentLayerFeaturesMapNV[nv])
}
}
// Insert FeatureVersions in the database.
addIDs, err := pgSQL.insertFeatureVersions(add)
if err != nil {
return err
}
delIDs, err := pgSQL.insertFeatureVersions(del)
if err != nil {
return err
}
// Insert diff in the database.
if len(addIDs) > 0 {
_, err = tx.Exec(insertLayerDiffFeatureVersion, layer.ID, "add", buildInputArray(addIDs))
if err != nil {
return handleError("insertLayerDiffFeatureVersion.Add", err)
}
}
if len(delIDs) > 0 {
_, err = tx.Exec(insertLayerDiffFeatureVersion, layer.ID, "del", buildInputArray(delIDs))
if err != nil {
return handleError("insertLayerDiffFeatureVersion.Del", err)
}
}
return nil
}
func createNV(features []database.FeatureVersion) (map[string]*database.FeatureVersion, []string) {
mapNV := make(map[string]*database.FeatureVersion, 0)
sliceNV := make([]string, 0, len(features))
for i := 0; i < len(features); i++ {
fv := &features[i]
nv := strings.Join([]string{fv.Feature.Namespace.Name, fv.Feature.Name, fv.Version}, ":")
mapNV[nv] = fv
sliceNV = append(sliceNV, nv)
}
return mapNV, sliceNV
}
func (pgSQL *pgSQL) DeleteLayer(name string) error {
defer observeQueryTime("DeleteLayer", "all", time.Now())
result, err := pgSQL.Exec(removeLayer, name)
if err != nil {
return handleError("removeLayer", err)
}
affected, err := result.RowsAffected()
if err != nil {
return handleError("removeLayer.RowsAffected()", err)
}
if affected <= 0 {
return commonerr.ErrNotFound
}
return nil
}

@ -0,0 +1,177 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package layer
import (
"database/sql"
"github.com/deckarep/golang-set"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/detector"
"github.com/coreos/clair/database/pgsql/util"
"github.com/coreos/clair/pkg/commonerr"
)
const (
soiLayer = `
WITH new_layer AS (
INSERT INTO layer (hash)
SELECT CAST ($1 AS VARCHAR)
WHERE NOT EXISTS (SELECT id FROM layer WHERE hash = $1)
RETURNING id
)
SELECT id FROM new_Layer
UNION
SELECT id FROM layer WHERE hash = $1`
findLayerID = `SELECT id FROM layer WHERE hash = $1`
)
func FindLayer(tx *sql.Tx, hash string) (database.Layer, bool, error) {
layer := database.Layer{Hash: hash}
if hash == "" {
return layer, false, commonerr.NewBadRequestError("non empty layer hash is expected.")
}
layerID, ok, err := FindLayerID(tx, hash)
if err != nil || !ok {
return layer, ok, err
}
detectorMap, err := detector.FindAllDetectors(tx)
if err != nil {
return layer, false, err
}
if layer.By, err = FindLayerDetectors(tx, layerID); err != nil {
return layer, false, err
}
if layer.Features, err = FindLayerFeatures(tx, layerID, detectorMap); err != nil {
return layer, false, err
}
if layer.Namespaces, err = FindLayerNamespaces(tx, layerID, detectorMap); err != nil {
return layer, false, err
}
return layer, true, nil
}
func sanitizePersistLayerInput(hash string, features []database.LayerFeature, namespaces []database.LayerNamespace, detectedBy []database.Detector) error {
if hash == "" {
return commonerr.NewBadRequestError("expected non-empty layer hash")
}
detectedBySet := mapset.NewSet()
for _, d := range detectedBy {
detectedBySet.Add(d)
}
for _, f := range features {
if !detectedBySet.Contains(f.By) {
return database.ErrInvalidParameters
}
}
for _, n := range namespaces {
if !detectedBySet.Contains(n.By) {
return database.ErrInvalidParameters
}
}
return nil
}
// PersistLayer saves the content of a layer to the database.
func PersistLayer(tx *sql.Tx, hash string, features []database.LayerFeature, namespaces []database.LayerNamespace, detectedBy []database.Detector) error {
var (
err error
id int64
detectorIDs []int64
)
if err = sanitizePersistLayerInput(hash, features, namespaces, detectedBy); err != nil {
return err
}
if id, err = SoiLayer(tx, hash); err != nil {
return err
}
if detectorIDs, err = detector.FindDetectorIDs(tx, detectedBy); err != nil {
if err == commonerr.ErrNotFound {
return database.ErrMissingEntities
}
return err
}
if err = PersistLayerDetectors(tx, id, detectorIDs); err != nil {
return err
}
if err = PersistAllLayerFeatures(tx, id, features); err != nil {
return err
}
if err = PersistAllLayerNamespaces(tx, id, namespaces); err != nil {
return err
}
return nil
}
func FindLayerID(tx *sql.Tx, hash string) (int64, bool, error) {
var layerID int64
err := tx.QueryRow(findLayerID, hash).Scan(&layerID)
if err != nil {
if err == sql.ErrNoRows {
return layerID, false, nil
}
return layerID, false, util.HandleError("findLayerID", err)
}
return layerID, true, nil
}
func FindLayerIDs(tx *sql.Tx, hashes []string) ([]int64, bool, error) {
layerIDs := make([]int64, 0, len(hashes))
for _, hash := range hashes {
id, ok, err := FindLayerID(tx, hash)
if !ok {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
layerIDs = append(layerIDs, id)
}
return layerIDs, true, nil
}
func SoiLayer(tx *sql.Tx, hash string) (int64, error) {
var id int64
if err := tx.QueryRow(soiLayer, hash).Scan(&id); err != nil {
return 0, util.HandleError("soiLayer", err)
}
return id, nil
}

@ -0,0 +1,66 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package layer
import (
"database/sql"
"github.com/deckarep/golang-set"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/detector"
"github.com/coreos/clair/database/pgsql/util"
)
const (
selectLayerDetectors = `
SELECT d.name, d.version, d.dtype
FROM layer_detector, detector AS d
WHERE layer_detector.detector_id = d.id AND layer_detector.layer_id = $1;`
persistLayerDetector = `
INSERT INTO layer_detector (layer_id, detector_id)
SELECT $1, $2
WHERE NOT EXISTS (SELECT id FROM layer_detector WHERE layer_id = $1 AND detector_id = $2)`
)
func PersistLayerDetector(tx *sql.Tx, layerID int64, detectorID int64) error {
if _, err := tx.Exec(persistLayerDetector, layerID, detectorID); err != nil {
return util.HandleError("persistLayerDetector", err)
}
return nil
}
func PersistLayerDetectors(tx *sql.Tx, layerID int64, detectorIDs []int64) error {
alreadySaved := mapset.NewSet()
for _, id := range detectorIDs {
if alreadySaved.Contains(id) {
continue
}
alreadySaved.Add(id)
if err := PersistLayerDetector(tx, layerID, id); err != nil {
return err
}
}
return nil
}
func FindLayerDetectors(tx *sql.Tx, id int64) ([]database.Detector, error) {
detectors, err := detector.GetDetectors(tx, selectLayerDetectors, id)
return detectors, err
}

@ -0,0 +1,147 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package layer
import (
"database/sql"
"sort"
"github.com/coreos/clair/database/pgsql/namespace"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/detector"
"github.com/coreos/clair/database/pgsql/feature"
"github.com/coreos/clair/database/pgsql/util"
)
const findLayerFeatures = `
SELECT
f.name, f.version, f.version_format, ft.name, lf.detector_id, ns.name, ns.version_format
FROM
layer_feature AS lf
LEFT JOIN feature f on f.id = lf.feature_id
LEFT JOIN feature_type ft on ft.id = f.type
LEFT JOIN namespace ns ON ns.id = lf.namespace_id
WHERE lf.layer_id = $1`
func queryPersistLayerFeature(count int) string {
return util.QueryPersist(count,
"layer_feature",
"layer_feature_layer_id_feature_id_namespace_id_key",
"layer_id",
"feature_id",
"detector_id",
"namespace_id")
}
// dbLayerFeature represents the layer_feature table
type dbLayerFeature struct {
layerID int64
featureID int64
detectorID int64
namespaceID sql.NullInt64
}
func FindLayerFeatures(tx *sql.Tx, layerID int64, detectors detector.DetectorMap) ([]database.LayerFeature, error) {
rows, err := tx.Query(findLayerFeatures, layerID)
if err != nil {
return nil, util.HandleError("findLayerFeatures", err)
}
defer rows.Close()
features := []database.LayerFeature{}
for rows.Next() {
var (
detectorID int64
feature database.LayerFeature
)
var namespaceName, namespaceVersion sql.NullString
if err := rows.Scan(&feature.Name, &feature.Version, &feature.VersionFormat, &feature.Type, &detectorID, &namespaceName, &namespaceVersion); err != nil {
return nil, util.HandleError("findLayerFeatures", err)
}
feature.PotentialNamespace.Name = namespaceName.String
feature.PotentialNamespace.VersionFormat = namespaceVersion.String
feature.By = detectors.ByID[detectorID]
features = append(features, feature)
}
return features, nil
}
func PersistAllLayerFeatures(tx *sql.Tx, layerID int64, features []database.LayerFeature) error {
detectorMap, err := detector.FindAllDetectors(tx)
if err != nil {
return err
}
var namespaces []database.Namespace
for _, feature := range features {
namespaces = append(namespaces, feature.PotentialNamespace)
}
nameSpaceIDs, _ := namespace.FindNamespaceIDs(tx, namespaces)
featureNamespaceMap := map[database.Namespace]sql.NullInt64{}
rawFeatures := make([]database.Feature, 0, len(features))
for i, f := range features {
rawFeatures = append(rawFeatures, f.Feature)
if f.PotentialNamespace.Valid() {
featureNamespaceMap[f.PotentialNamespace] = nameSpaceIDs[i]
}
}
featureIDs, err := feature.FindFeatureIDs(tx, rawFeatures)
if err != nil {
return err
}
var namespaceID sql.NullInt64
dbFeatures := make([]dbLayerFeature, 0, len(features))
for i, f := range features {
detectorID := detectorMap.ByValue[f.By]
featureID := featureIDs[i].Int64
if !featureIDs[i].Valid {
return database.ErrMissingEntities
}
namespaceID = featureNamespaceMap[f.PotentialNamespace]
dbFeatures = append(dbFeatures, dbLayerFeature{layerID, featureID, detectorID, namespaceID})
}
if err := PersistLayerFeatures(tx, dbFeatures); err != nil {
return err
}
return nil
}
func PersistLayerFeatures(tx *sql.Tx, features []dbLayerFeature) error {
if len(features) == 0 {
return nil
}
sort.Slice(features, func(i, j int) bool {
return features[i].featureID < features[j].featureID
})
keys := make([]interface{}, 0, len(features)*4)
for _, f := range features {
keys = append(keys, f.layerID, f.featureID, f.detectorID, f.namespaceID)
}
_, err := tx.Exec(queryPersistLayerFeature(len(features)), keys...)
if err != nil {
return util.HandleError("queryPersistLayerFeature", err)
}
return nil
}

@ -0,0 +1,127 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package layer
import (
"database/sql"
"sort"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/detector"
"github.com/coreos/clair/database/pgsql/namespace"
"github.com/coreos/clair/database/pgsql/util"
)
const findLayerNamespaces = `
SELECT ns.name, ns.version_format, ln.detector_id
FROM layer_namespace AS ln, namespace AS ns
WHERE ln.namespace_id = ns.id
AND ln.layer_id = $1`
func queryPersistLayerNamespace(count int) string {
return util.QueryPersist(count,
"layer_namespace",
"layer_namespace_layer_id_namespace_id_key",
"layer_id",
"namespace_id",
"detector_id")
}
// dbLayerNamespace represents the layer_namespace table.
type dbLayerNamespace struct {
layerID int64
namespaceID int64
detectorID int64
}
func FindLayerNamespaces(tx *sql.Tx, layerID int64, detectors detector.DetectorMap) ([]database.LayerNamespace, error) {
rows, err := tx.Query(findLayerNamespaces, layerID)
if err != nil {
return nil, util.HandleError("findLayerNamespaces", err)
}
namespaces := []database.LayerNamespace{}
for rows.Next() {
var (
namespace database.LayerNamespace
detectorID int64
)
if err := rows.Scan(&namespace.Name, &namespace.VersionFormat, &detectorID); err != nil {
return nil, err
}
namespace.By = detectors.ByID[detectorID]
namespaces = append(namespaces, namespace)
}
return namespaces, nil
}
func PersistAllLayerNamespaces(tx *sql.Tx, layerID int64, namespaces []database.LayerNamespace) error {
detectorMap, err := detector.FindAllDetectors(tx)
if err != nil {
return err
}
// TODO(sidac): This kind of type conversion is very useless and wasteful,
// we need interfaces around the database models to reduce these kind of
// operations.
rawNamespaces := make([]database.Namespace, 0, len(namespaces))
for _, ns := range namespaces {
rawNamespaces = append(rawNamespaces, ns.Namespace)
}
rawNamespaceIDs, err := namespace.FindNamespaceIDs(tx, rawNamespaces)
if err != nil {
return err
}
dbLayerNamespaces := make([]dbLayerNamespace, 0, len(namespaces))
for i, ns := range namespaces {
detectorID := detectorMap.ByValue[ns.By]
namespaceID := rawNamespaceIDs[i].Int64
if !rawNamespaceIDs[i].Valid {
return database.ErrMissingEntities
}
dbLayerNamespaces = append(dbLayerNamespaces, dbLayerNamespace{layerID, namespaceID, detectorID})
}
return PersistLayerNamespaces(tx, dbLayerNamespaces)
}
func PersistLayerNamespaces(tx *sql.Tx, namespaces []dbLayerNamespace) error {
if len(namespaces) == 0 {
return nil
}
// for every bulk persist operation, the input data should be sorted.
sort.Slice(namespaces, func(i, j int) bool {
return namespaces[i].namespaceID < namespaces[j].namespaceID
})
keys := make([]interface{}, 0, len(namespaces)*3)
for _, row := range namespaces {
keys = append(keys, row.layerID, row.namespaceID, row.detectorID)
}
_, err := tx.Exec(queryPersistLayerNamespace(len(namespaces)), keys...)
if err != nil {
return util.HandleError("queryPersistLayerNamespace", err)
}
return nil
}

@ -0,0 +1,214 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package layer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/testutil"
)
var persistLayerTests = []struct {
title string
name string
by []database.Detector
features []database.LayerFeature
namespaces []database.LayerNamespace
layer *database.Layer
err string
}{
{
title: "invalid layer name",
name: "",
err: "expected non-empty layer hash",
},
{
title: "layer with inconsistent feature and detectors",
name: "random-forest",
by: []database.Detector{testutil.RealDetectors[2]},
features: []database.LayerFeature{
{testutil.RealFeatures[1], testutil.RealDetectors[1], database.Namespace{}},
},
err: "parameters are not valid",
},
{
title: "layer with non-existing feature",
name: "random-forest",
err: "associated immutable entities are missing in the database",
by: []database.Detector{testutil.RealDetectors[2]},
features: []database.LayerFeature{
{testutil.FakeFeatures[1], testutil.RealDetectors[2], database.Namespace{}},
},
},
{
title: "layer with non-existing namespace",
name: "random-forest2",
err: "associated immutable entities are missing in the database",
by: []database.Detector{testutil.RealDetectors[1]},
namespaces: []database.LayerNamespace{
{testutil.FakeNamespaces[1], testutil.RealDetectors[1]},
},
},
{
title: "layer with non-existing detector",
name: "random-forest3",
err: "associated immutable entities are missing in the database",
by: []database.Detector{testutil.FakeDetector[1]},
},
{
title: "valid layer",
name: "hamsterhouse",
by: []database.Detector{testutil.RealDetectors[1], testutil.RealDetectors[2]},
features: []database.LayerFeature{
{testutil.RealFeatures[1], testutil.RealDetectors[2], database.Namespace{}},
{testutil.RealFeatures[2], testutil.RealDetectors[2], database.Namespace{}},
},
namespaces: []database.LayerNamespace{
{testutil.RealNamespaces[1], testutil.RealDetectors[1]},
},
layer: &database.Layer{
Hash: "hamsterhouse",
By: []database.Detector{testutil.RealDetectors[1], testutil.RealDetectors[2]},
Features: []database.LayerFeature{
{testutil.RealFeatures[1], testutil.RealDetectors[2], database.Namespace{}},
{testutil.RealFeatures[2], testutil.RealDetectors[2], database.Namespace{}},
},
Namespaces: []database.LayerNamespace{
{testutil.RealNamespaces[1], testutil.RealDetectors[1]},
},
},
},
{
title: "update existing layer",
name: "layer-1",
by: []database.Detector{testutil.RealDetectors[3], testutil.RealDetectors[4]},
features: []database.LayerFeature{
{testutil.RealFeatures[4], testutil.RealDetectors[3], database.Namespace{}},
},
namespaces: []database.LayerNamespace{
{testutil.RealNamespaces[3], testutil.RealDetectors[4]},
},
layer: &database.Layer{
Hash: "layer-1",
By: []database.Detector{testutil.RealDetectors[1], testutil.RealDetectors[2], testutil.RealDetectors[3], testutil.RealDetectors[4]},
Features: []database.LayerFeature{
{testutil.RealFeatures[1], testutil.RealDetectors[2], database.Namespace{}},
{testutil.RealFeatures[2], testutil.RealDetectors[2], database.Namespace{}},
{testutil.RealFeatures[4], testutil.RealDetectors[3], database.Namespace{}},
},
Namespaces: []database.LayerNamespace{
{testutil.RealNamespaces[1], testutil.RealDetectors[1]},
{testutil.RealNamespaces[3], testutil.RealDetectors[4]},
},
},
},
{
title: "layer with potential namespace",
name: "layer-potential-namespace",
by: []database.Detector{testutil.RealDetectors[3]},
features: []database.LayerFeature{
{testutil.RealFeatures[4], testutil.RealDetectors[3], testutil.RealNamespaces[4]},
},
namespaces: []database.LayerNamespace{
{testutil.RealNamespaces[3], testutil.RealDetectors[3]},
},
layer: &database.Layer{
Hash: "layer-potential-namespace",
By: []database.Detector{testutil.RealDetectors[3]},
Features: []database.LayerFeature{
{testutil.RealFeatures[4], testutil.RealDetectors[3], testutil.RealNamespaces[4]},
},
Namespaces: []database.LayerNamespace{
{testutil.RealNamespaces[3], testutil.RealDetectors[3]},
},
},
},
}
func TestPersistLayer(t *testing.T) {
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "PersistLayer")
defer cleanup()
for _, test := range persistLayerTests {
t.Run(test.title, func(t *testing.T) {
err := PersistLayer(tx, test.name, test.features, test.namespaces, test.by)
if test.err != "" {
assert.EqualError(t, err, test.err, "unexpected error")
return
}
assert.Nil(t, err)
if test.layer != nil {
layer, ok, err := FindLayer(tx, test.name)
assert.Nil(t, err)
assert.True(t, ok)
database.AssertLayerEqual(t, test.layer, &layer)
}
})
}
}
var findLayerTests = []struct {
title string
in string
out *database.Layer
err string
ok bool
}{
{
title: "invalid layer name",
in: "",
err: "non empty layer hash is expected.",
},
{
title: "non-existing layer",
in: "layer-non-existing",
ok: false,
out: nil,
},
{
title: "existing layer",
in: "layer-4",
ok: true,
out: testutil.TakeLayerPointerFromMap(testutil.RealLayers, 6),
},
}
func TestFindLayer(t *testing.T) {
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "FindLayer")
defer cleanup()
for _, test := range findLayerTests {
t.Run(test.title, func(t *testing.T) {
layer, ok, err := FindLayer(tx, test.in)
if test.err != "" {
assert.EqualError(t, err, test.err, "unexpected error")
return
}
assert.Nil(t, err)
assert.Equal(t, test.ok, ok)
if test.ok {
database.AssertLayerEqual(t, test.out, &layer)
}
})
}
}

@ -1,384 +0,0 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pgsql
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt/dpkg"
"github.com/coreos/clair/pkg/commonerr"
)
func TestFindLayer(t *testing.T) {
datastore, err := openDatabaseForTest("FindLayer", true)
if err != nil {
t.Error(err)
return
}
defer datastore.Close()
// Layer-0: no parent, no namespace, no feature, no vulnerability
layer, err := datastore.FindLayer("layer-0", false, false)
if assert.Nil(t, err) && assert.NotNil(t, layer) {
assert.Equal(t, "layer-0", layer.Name)
assert.Nil(t, layer.Namespace)
assert.Nil(t, layer.Parent)
assert.Equal(t, 1, layer.EngineVersion)
assert.Len(t, layer.Features, 0)
}
layer, err = datastore.FindLayer("layer-0", true, false)
if assert.Nil(t, err) && assert.NotNil(t, layer) {
assert.Len(t, layer.Features, 0)
}
// Layer-1: one parent, adds two features, one vulnerability
layer, err = datastore.FindLayer("layer-1", false, false)
if assert.Nil(t, err) && assert.NotNil(t, layer) {
assert.Equal(t, layer.Name, "layer-1")
assert.Equal(t, "debian:7", layer.Namespace.Name)
if assert.NotNil(t, layer.Parent) {
assert.Equal(t, "layer-0", layer.Parent.Name)
}
assert.Equal(t, 1, layer.EngineVersion)
assert.Len(t, layer.Features, 0)
}
layer, err = datastore.FindLayer("layer-1", true, false)
if assert.Nil(t, err) && assert.NotNil(t, layer) && assert.Len(t, layer.Features, 2) {
for _, featureVersion := range layer.Features {
assert.Equal(t, "debian:7", featureVersion.Feature.Namespace.Name)
switch featureVersion.Feature.Name {
case "wechat":
assert.Equal(t, "0.5", featureVersion.Version)
case "openssl":
assert.Equal(t, "1.0", featureVersion.Version)
default:
t.Errorf("unexpected package %s for layer-1", featureVersion.Feature.Name)
}
}
}
layer, err = datastore.FindLayer("layer-1", true, true)
if assert.Nil(t, err) && assert.NotNil(t, layer) && assert.Len(t, layer.Features, 2) {
for _, featureVersion := range layer.Features {
assert.Equal(t, "debian:7", featureVersion.Feature.Namespace.Name)
switch featureVersion.Feature.Name {
case "wechat":
assert.Equal(t, "0.5", featureVersion.Version)
case "openssl":
assert.Equal(t, "1.0", featureVersion.Version)
if assert.Len(t, featureVersion.AffectedBy, 1) {
assert.Equal(t, "debian:7", featureVersion.AffectedBy[0].Namespace.Name)
assert.Equal(t, "CVE-OPENSSL-1-DEB7", featureVersion.AffectedBy[0].Name)
assert.Equal(t, database.HighSeverity, featureVersion.AffectedBy[0].Severity)
assert.Equal(t, "A vulnerability affecting OpenSSL < 2.0 on Debian 7.0", featureVersion.AffectedBy[0].Description)
assert.Equal(t, "http://google.com/#q=CVE-OPENSSL-1-DEB7", featureVersion.AffectedBy[0].Link)
assert.Equal(t, "2.0", featureVersion.AffectedBy[0].FixedBy)
}
default:
t.Errorf("unexpected package %s for layer-1", featureVersion.Feature.Name)
}
}
}
}
func TestInsertLayer(t *testing.T) {
datastore, err := openDatabaseForTest("InsertLayer", false)
if err != nil {
t.Error(err)
return
}
defer datastore.Close()
// Insert invalid layer.
testInsertLayerInvalid(t, datastore)
// Insert a layer tree.
testInsertLayerTree(t, datastore)
// Update layer.
testInsertLayerUpdate(t, datastore)
// Delete layer.
testInsertLayerDelete(t, datastore)
}
func testInsertLayerInvalid(t *testing.T, datastore database.Datastore) {
invalidLayers := []database.Layer{
{},
{Name: "layer0", Parent: &database.Layer{}},
{Name: "layer0", Parent: &database.Layer{Name: "UnknownLayer"}},
}
for _, invalidLayer := range invalidLayers {
err := datastore.InsertLayer(invalidLayer)
assert.Error(t, err)
}
}
func testInsertLayerTree(t *testing.T, datastore database.Datastore) {
f1 := database.FeatureVersion{
Feature: database.Feature{
Namespace: database.Namespace{
Name: "TestInsertLayerNamespace2",
VersionFormat: dpkg.ParserName,
},
Name: "TestInsertLayerFeature1",
},
Version: "1.0",
}
f2 := database.FeatureVersion{
Feature: database.Feature{
Namespace: database.Namespace{
Name: "TestInsertLayerNamespace2",
VersionFormat: dpkg.ParserName,
},
Name: "TestInsertLayerFeature2",
},
Version: "0.34",
}
f3 := database.FeatureVersion{
Feature: database.Feature{
Namespace: database.Namespace{
Name: "TestInsertLayerNamespace2",
VersionFormat: dpkg.ParserName,
},
Name: "TestInsertLayerFeature3",
},
Version: "0.56",
}
f4 := database.FeatureVersion{
Feature: database.Feature{
Namespace: database.Namespace{
Name: "TestInsertLayerNamespace3",
VersionFormat: dpkg.ParserName,
},
Name: "TestInsertLayerFeature2",
},
Version: "0.34",
}
f5 := database.FeatureVersion{
Feature: database.Feature{
Namespace: database.Namespace{
Name: "TestInsertLayerNamespace3",
VersionFormat: dpkg.ParserName,
},
Name: "TestInsertLayerFeature3",
},
Version: "0.56",
}
f6 := database.FeatureVersion{
Feature: database.Feature{
Namespace: database.Namespace{
Name: "TestInsertLayerNamespace3",
VersionFormat: dpkg.ParserName,
},
Name: "TestInsertLayerFeature4",
},
Version: "0.666",
}
layers := []database.Layer{
{
Name: "TestInsertLayer1",
},
{
Name: "TestInsertLayer2",
Parent: &database.Layer{Name: "TestInsertLayer1"},
Namespace: &database.Namespace{
Name: "TestInsertLayerNamespace1",
VersionFormat: dpkg.ParserName,
},
},
// This layer changes the namespace and adds Features.
{
Name: "TestInsertLayer3",
Parent: &database.Layer{Name: "TestInsertLayer2"},
Namespace: &database.Namespace{
Name: "TestInsertLayerNamespace2",
VersionFormat: dpkg.ParserName,
},
Features: []database.FeatureVersion{f1, f2, f3},
},
// This layer covers the case where the last layer doesn't provide any new Feature.
{
Name: "TestInsertLayer4a",
Parent: &database.Layer{Name: "TestInsertLayer3"},
Features: []database.FeatureVersion{f1, f2, f3},
},
// This layer covers the case where the last layer provides Features.
// It also modifies the Namespace ("upgrade") but keeps some Features not upgraded, their
// Namespaces should then remain unchanged.
{
Name: "TestInsertLayer4b",
Parent: &database.Layer{Name: "TestInsertLayer3"},
Namespace: &database.Namespace{
Name: "TestInsertLayerNamespace3",
VersionFormat: dpkg.ParserName,
},
Features: []database.FeatureVersion{
// Deletes TestInsertLayerFeature1.
// Keep TestInsertLayerFeature2 (old Namespace should be kept):
f4,
// Upgrades TestInsertLayerFeature3 (with new Namespace):
f5,
// Adds TestInsertLayerFeature4:
f6,
},
},
}
var err error
retrievedLayers := make(map[string]database.Layer)
for _, layer := range layers {
if layer.Parent != nil {
// Retrieve from database its parent and assign.
parent := retrievedLayers[layer.Parent.Name]
layer.Parent = &parent
}
err = datastore.InsertLayer(layer)
assert.Nil(t, err)
retrievedLayers[layer.Name], err = datastore.FindLayer(layer.Name, true, false)
assert.Nil(t, err)
}
l4a := retrievedLayers["TestInsertLayer4a"]
if assert.NotNil(t, l4a.Namespace) {
assert.Equal(t, "TestInsertLayerNamespace2", l4a.Namespace.Name)
}
assert.Len(t, l4a.Features, 3)
for _, featureVersion := range l4a.Features {
if cmpFV(featureVersion, f1) && cmpFV(featureVersion, f2) && cmpFV(featureVersion, f3) {
assert.Error(t, fmt.Errorf("TestInsertLayer4a contains an unexpected package: %#v. Should contain %#v and %#v and %#v.", featureVersion, f1, f2, f3))
}
}
l4b := retrievedLayers["TestInsertLayer4b"]
if assert.NotNil(t, l4b.Namespace) {
assert.Equal(t, "TestInsertLayerNamespace3", l4b.Namespace.Name)
}
assert.Len(t, l4b.Features, 3)
for _, featureVersion := range l4b.Features {
if cmpFV(featureVersion, f2) && cmpFV(featureVersion, f5) && cmpFV(featureVersion, f6) {
assert.Error(t, fmt.Errorf("TestInsertLayer4a contains an unexpected package: %#v. Should contain %#v and %#v and %#v.", featureVersion, f2, f4, f6))
}
}
}
func testInsertLayerUpdate(t *testing.T, datastore database.Datastore) {
f7 := database.FeatureVersion{
Feature: database.Feature{
Namespace: database.Namespace{
Name: "TestInsertLayerNamespace3",
VersionFormat: dpkg.ParserName,
},
Name: "TestInsertLayerFeature7",
},
Version: "0.01",
}
l3, _ := datastore.FindLayer("TestInsertLayer3", true, false)
l3u := database.Layer{
Name: l3.Name,
Parent: l3.Parent,
Namespace: &database.Namespace{
Name: "TestInsertLayerNamespaceUpdated1",
VersionFormat: dpkg.ParserName,
},
Features: []database.FeatureVersion{f7},
}
l4u := database.Layer{
Name: "TestInsertLayer4",
Parent: &database.Layer{Name: "TestInsertLayer3"},
Features: []database.FeatureVersion{f7},
EngineVersion: 2,
}
// Try to re-insert without increasing the EngineVersion.
err := datastore.InsertLayer(l3u)
assert.Nil(t, err)
l3uf, err := datastore.FindLayer(l3u.Name, true, false)
if assert.Nil(t, err) {
assert.Equal(t, l3.Namespace.Name, l3uf.Namespace.Name)
assert.Equal(t, l3.EngineVersion, l3uf.EngineVersion)
assert.Len(t, l3uf.Features, len(l3.Features))
}
// Update layer l3.
// Verify that the Namespace, EngineVersion and FeatureVersions got updated.
l3u.EngineVersion = 2
err = datastore.InsertLayer(l3u)
assert.Nil(t, err)
l3uf, err = datastore.FindLayer(l3u.Name, true, false)
if assert.Nil(t, err) {
assert.Equal(t, l3u.Namespace.Name, l3uf.Namespace.Name)
assert.Equal(t, l3u.EngineVersion, l3uf.EngineVersion)
if assert.Len(t, l3uf.Features, 1) {
assert.True(t, cmpFV(l3uf.Features[0], f7), "Updated layer should have %#v but actually have %#v", f7, l3uf.Features[0])
}
}
// Update layer l4.
// Verify that the Namespace got updated from its new Parent's, and also verify the
// EnginVersion and FeatureVersions.
l4u.Parent = &l3uf
err = datastore.InsertLayer(l4u)
assert.Nil(t, err)
l4uf, err := datastore.FindLayer(l3u.Name, true, false)
if assert.Nil(t, err) {
assert.Equal(t, l3u.Namespace.Name, l4uf.Namespace.Name)
assert.Equal(t, l4u.EngineVersion, l4uf.EngineVersion)
if assert.Len(t, l4uf.Features, 1) {
assert.True(t, cmpFV(l3uf.Features[0], f7), "Updated layer should have %#v but actually have %#v", f7, l4uf.Features[0])
}
}
}
func testInsertLayerDelete(t *testing.T, datastore database.Datastore) {
err := datastore.DeleteLayer("TestInsertLayerX")
assert.Equal(t, commonerr.ErrNotFound, err)
err = datastore.DeleteLayer("TestInsertLayer3")
assert.Nil(t, err)
_, err = datastore.FindLayer("TestInsertLayer3", false, false)
assert.Equal(t, commonerr.ErrNotFound, err)
_, err = datastore.FindLayer("TestInsertLayer4a", false, false)
assert.Equal(t, commonerr.ErrNotFound, err)
_, err = datastore.FindLayer("TestInsertLayer4b", true, false)
assert.Equal(t, commonerr.ErrNotFound, err)
}
func cmpFV(a, b database.FeatureVersion) bool {
return a.Feature.Name == b.Feature.Name &&
a.Feature.Namespace.Name == b.Feature.Namespace.Name &&
a.Version == b.Version
}

@ -1,107 +0,0 @@
// Copyright 2017 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pgsql
import (
"time"
log "github.com/sirupsen/logrus"
"github.com/coreos/clair/pkg/commonerr"
)
// Lock tries to set a temporary lock in the database.
//
// Lock does not block, instead, it returns true and its expiration time
// is the lock has been successfully acquired or false otherwise
func (pgSQL *pgSQL) Lock(name string, owner string, duration time.Duration, renew bool) (bool, time.Time) {
if name == "" || owner == "" || duration == 0 {
log.Warning("could not create an invalid lock")
return false, time.Time{}
}
defer observeQueryTime("Lock", "all", time.Now())
// Compute expiration.
until := time.Now().Add(duration)
if renew {
// Renew lock.
r, err := pgSQL.Exec(updateLock, name, owner, until)
if err != nil {
handleError("updateLock", err)
return false, until
}
if n, _ := r.RowsAffected(); n > 0 {
// Updated successfully.
return true, until
}
} else {
// Prune locks.
pgSQL.pruneLocks()
}
// Lock.
_, err := pgSQL.Exec(insertLock, name, owner, until)
if err != nil {
if !isErrUniqueViolation(err) {
handleError("insertLock", err)
}
return false, until
}
return true, until
}
// Unlock unlocks a lock specified by its name if I own it
func (pgSQL *pgSQL) Unlock(name, owner string) {
if name == "" || owner == "" {
log.Warning("could not delete an invalid lock")
return
}
defer observeQueryTime("Unlock", "all", time.Now())
pgSQL.Exec(removeLock, name, owner)
}
// FindLock returns the owner of a lock specified by its name and its
// expiration time.
func (pgSQL *pgSQL) FindLock(name string) (string, time.Time, error) {
if name == "" {
log.Warning("could not find an invalid lock")
return "", time.Time{}, commonerr.NewBadRequestError("could not find an invalid lock")
}
defer observeQueryTime("FindLock", "all", time.Now())
var owner string
var until time.Time
err := pgSQL.QueryRow(searchLock, name).Scan(&owner, &until)
if err != nil {
return owner, until, handleError("searchLock", err)
}
return owner, until, nil
}
// pruneLocks removes every expired locks from the database
func (pgSQL *pgSQL) pruneLocks() {
defer observeQueryTime("pruneLocks", "all", time.Now())
if _, err := pgSQL.Exec(removeLockExpired); err != nil {
handleError("removeLockExpired", err)
}
}

@ -0,0 +1,109 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package lock
import (
"database/sql"
"time"
"github.com/coreos/clair/database/pgsql/monitoring"
"github.com/coreos/clair/database/pgsql/util"
log "github.com/sirupsen/logrus"
)
const (
searchLock = `SELECT until FROM Lock WHERE name = $1`
updateLock = `UPDATE Lock SET until = $3 WHERE name = $1 AND owner = $2`
removeLock = `DELETE FROM Lock WHERE name = $1 AND owner = $2`
removeLockExpired = `DELETE FROM LOCK WHERE until < $1`
soiLock = `
WITH new_lock AS (
INSERT INTO lock (name, owner, until)
SELECT CAST ($1 AS TEXT), CAST ($2 AS TEXT), CAST ($3 AS TIMESTAMP)
WHERE NOT EXISTS (SELECT id FROM lock WHERE name = $1)
RETURNING owner, until
)
SELECT * FROM new_lock
UNION
SELECT owner, until FROM lock WHERE name = $1`
)
func AcquireLock(tx *sql.Tx, lockName, whoami string, desiredDuration time.Duration) (bool, time.Time, error) {
if lockName == "" || whoami == "" || desiredDuration == 0 {
panic("invalid lock parameters")
}
if err := PruneLocks(tx); err != nil {
return false, time.Time{}, err
}
var (
desiredLockedUntil = time.Now().UTC().Add(desiredDuration)
lockedUntil time.Time
lockOwner string
)
defer monitoring.ObserveQueryTime("Lock", "soiLock", time.Now())
err := tx.QueryRow(soiLock, lockName, whoami, desiredLockedUntil).Scan(&lockOwner, &lockedUntil)
return lockOwner == whoami, lockedUntil, util.HandleError("AcquireLock", err)
}
func ExtendLock(tx *sql.Tx, lockName, whoami string, desiredDuration time.Duration) (bool, time.Time, error) {
if lockName == "" || whoami == "" || desiredDuration == 0 {
panic("invalid lock parameters")
}
desiredLockedUntil := time.Now().Add(desiredDuration)
defer monitoring.ObserveQueryTime("Lock", "update", time.Now())
result, err := tx.Exec(updateLock, lockName, whoami, desiredLockedUntil)
if err != nil {
return false, time.Time{}, util.HandleError("updateLock", err)
}
if numRows, err := result.RowsAffected(); err == nil {
// This is the only happy path.
return numRows > 0, desiredLockedUntil, nil
}
return false, time.Time{}, util.HandleError("updateLock", err)
}
func ReleaseLock(tx *sql.Tx, name, owner string) error {
if name == "" || owner == "" {
panic("invalid lock parameters")
}
defer monitoring.ObserveQueryTime("Unlock", "all", time.Now())
_, err := tx.Exec(removeLock, name, owner)
return err
}
// pruneLocks removes every expired locks from the database
func PruneLocks(tx *sql.Tx) error {
defer monitoring.ObserveQueryTime("pruneLocks", "all", time.Now())
if r, err := tx.Exec(removeLockExpired, time.Now().UTC()); err != nil {
return util.HandleError("removeLockExpired", err)
} else if affected, err := r.RowsAffected(); err != nil {
return util.HandleError("removeLockExpired", err)
} else {
log.Debugf("Pruned %d Locks", affected)
}
return nil
}

@ -0,0 +1,100 @@
// Copyright 2019 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package lock
import (
"testing"
"time"
"github.com/coreos/clair/database/pgsql/testutil"
"github.com/stretchr/testify/require"
)
func TestAcquireLockReturnsExistingLockDuration(t *testing.T) {
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "Lock")
defer cleanup()
acquired, originalExpiration, err := AcquireLock(tx, "test1", "owner1", time.Minute)
require.Nil(t, err)
require.True(t, acquired)
acquired2, expiration, err := AcquireLock(tx, "test1", "owner2", time.Hour)
require.Nil(t, err)
require.False(t, acquired2)
require.Equal(t, expiration, originalExpiration)
}
func TestLock(t *testing.T) {
db, cleanup := testutil.CreateTestDBWithFixture(t, "Lock")
defer cleanup()
tx, err := db.Begin()
if err != nil {
panic(err)
}
// Create a first lock.
l, _, err := AcquireLock(tx, "test1", "owner1", time.Minute)
require.Nil(t, err)
require.True(t, l)
tx = testutil.RestartTransaction(db, tx, true)
// lock again by itself, the previous lock is not expired yet.
l, _, err = AcquireLock(tx, "test1", "owner1", time.Minute)
require.Nil(t, err)
require.True(t, l)
tx = testutil.RestartTransaction(db, tx, false)
// Try to renew the same lock with another owner.
l, _, err = ExtendLock(tx, "test1", "owner2", time.Minute)
require.Nil(t, err)
require.False(t, l)
tx = testutil.RestartTransaction(db, tx, false)
l, _, err = AcquireLock(tx, "test1", "owner2", time.Minute)
require.Nil(t, err)
require.False(t, l)
tx = testutil.RestartTransaction(db, tx, false)
// Renew the lock.
l, _, err = ExtendLock(tx, "test1", "owner1", 2*time.Minute)
require.Nil(t, err)
require.True(t, l)
tx = testutil.RestartTransaction(db, tx, true)
// Unlock and then relock by someone else.
err = ReleaseLock(tx, "test1", "owner1")
require.Nil(t, err)
tx = testutil.RestartTransaction(db, tx, true)
l, _, err = AcquireLock(tx, "test1", "owner2", time.Minute)
require.Nil(t, err)
require.True(t, l)
tx = testutil.RestartTransaction(db, tx, true)
// Create a second lock which is actually already expired ...
l, _, err = AcquireLock(tx, "test2", "owner1", -time.Minute)
require.Nil(t, err)
require.True(t, l)
tx = testutil.RestartTransaction(db, tx, true)
// Take over the lock
l, _, err = AcquireLock(tx, "test2", "owner2", time.Minute)
require.Nil(t, err)
require.True(t, l)
tx = testutil.RestartTransaction(db, tx, true)
require.Nil(t, tx.Rollback())
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save