func findTask(datastore database.Datastore, renotifyInterval time.Duration, whoAmI string, stopper *utils.Stopper) *database.VulnerabilityNotification { for { // Find a notification to send. notification, err := datastore.GetAvailableNotification(renotifyInterval) if err != nil { // There is no notification or an error occurred. if err != cerrors.ErrNotFound { log.Warningf("could not get notification to send: %s", err) } // Wait. if !stopper.Sleep(checkInterval) { return nil } continue } // Lock the notification. if hasLock, _ := datastore.Lock(notification.Name, whoAmI, lockDuration, false); hasLock { log.Infof("found and locked a notification: %s", notification.Name) return ¬ification } } }
// FetchUpdate fetches vulnerability updates from the Debian Security Tracker. func (fetcher *DebianFetcher) FetchUpdate(datastore database.Datastore) (resp updater.FetcherResponse, err error) { log.Info("fetching Debian vulnerabilities") // Download JSON. r, err := http.Get(url) if err != nil { log.Errorf("could not download Debian's update: %s", err) io.Copy(ioutil.Discard, r.Body) return resp, cerrors.ErrCouldNotDownload } defer r.Body.Close() r.Close = true // Get the SHA-1 of the latest update's JSON data latestHash, err := datastore.GetKeyValue(updaterFlag) if err != nil { return resp, err } // Parse the JSON. resp, err = buildResponse(r.Body, latestHash) if err != nil { return resp, err } return resp, nil }
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 testInsertLayerDelete(t *testing.T, datastore database.Datastore) { err := datastore.DeleteLayer("TestInsertLayerX") assert.Equal(t, cerrors.ErrNotFound, err) err = datastore.DeleteLayer("TestInsertLayer3") assert.Nil(t, err) _, err = datastore.FindLayer("TestInsertLayer3", false, false) assert.Equal(t, cerrors.ErrNotFound, err) _, err = datastore.FindLayer("TestInsertLayer4a", false, false) assert.Equal(t, cerrors.ErrNotFound, err) _, err = datastore.FindLayer("TestInsertLayer4b", true, false) assert.Equal(t, cerrors.ErrNotFound, err) }
func (f *fetcher) FetchUpdate(db database.Datastore) (resp updater.FetcherResponse, err error) { log.Info("fetching Alpine vulnerabilities") // Pull the master branch. var commit string commit, err = f.pullRepository() if err != nil { return } // Ask the database for the latest commit we successfully applied. var dbCommit string dbCommit, err = db.GetKeyValue(updaterFlag) if err != nil { return } // Set the updaterFlag to equal the commit processed. resp.FlagName = updaterFlag resp.FlagValue = commit // Short-circuit if there have been no updates. if commit == dbCommit { log.Debug("no alpine update") return } var namespaces []string namespaces, err = detectNamespaces(f.repositoryLocalPath) // Append any changed vulnerabilities to the response. for _, namespace := range namespaces { var vulns []database.Vulnerability var note string vulns, note, err = parseVulnsFromNamespace(f.repositoryLocalPath, namespace) if err != nil { return } if note != "" { resp.Notes = append(resp.Notes, note) } resp.Vulnerabilities = append(resp.Vulnerabilities, vulns...) } return }
func getLastUpdate(datastore database.Datastore) (time.Time, bool, error) { lastUpdateTSS, err := datastore.GetKeyValue(flagName) if err != nil { return time.Time{}, false, err } if lastUpdateTSS == "" { // This is the first update. return time.Time{}, true, nil } lastUpdateTS, err := strconv.ParseInt(lastUpdateTSS, 10, 64) if err != nil { return time.Time{}, false, err } return time.Unix(lastUpdateTS, 0).UTC(), false, nil }
// Update fetches all the vulnerabilities from the registered fetchers, upserts // them into the database and then sends notifications. func Update(datastore database.Datastore, firstUpdate bool) { defer setUpdaterDuration(time.Now()) log.Info("updating vulnerabilities") // Fetch updates. status, vulnerabilities, flags, notes := fetch(datastore) // Insert vulnerabilities. log.Tracef("inserting %d vulnerabilities for update", len(vulnerabilities)) err := datastore.InsertVulnerabilities(vulnerabilities, !firstUpdate) if err != nil { promUpdaterErrorsTotal.Inc() log.Errorf("an error occured when inserting vulnerabilities for update: %s", err) return } vulnerabilities = nil // Update flags. for flagName, flagValue := range flags { datastore.InsertKeyValue(flagName, flagValue) } // Log notes. for _, note := range notes { log.Warningf("fetcher note: %s", note) } promUpdaterNotesTotal.Set(float64(len(notes))) // Update last successful update if every fetchers worked properly. if status { datastore.InsertKeyValue(flagName, strconv.FormatInt(time.Now().UTC().Unix(), 10)) } log.Info("update finished") }
// Process detects the Namespace of a layer, the features it adds/removes, and // then stores everything in the database. // TODO(Quentin-M): We could have a goroutine that looks for layers that have been analyzed with an // older engine version and that processes them. func Process(datastore database.Datastore, imageFormat, name, parentName, path string, headers map[string]string) error { // Verify parameters. if name == "" { return cerrors.NewBadRequestError("could not process a layer which does not have a name") } if path == "" { return cerrors.NewBadRequestError("could not process a layer which does not have a path") } if imageFormat == "" { return cerrors.NewBadRequestError("could not process a layer which does not have a format") } log.Debugf("layer %s: processing (Location: %s, Engine version: %d, Parent: %s, Format: %s)", name, utils.CleanURL(path), Version, parentName, imageFormat) // Check to see if the layer is already in the database. layer, err := datastore.FindLayer(name, false, false) if err != nil && err != cerrors.ErrNotFound { return err } if err == cerrors.ErrNotFound { // New layer case. layer = database.Layer{Name: name, EngineVersion: Version} // Retrieve the parent if it has one. // We need to get it with its Features in order to diff them. if parentName != "" { parent, err := datastore.FindLayer(parentName, true, false) if err != nil && err != cerrors.ErrNotFound { return err } if err == cerrors.ErrNotFound { log.Warningf("layer %s: the parent layer (%s) is unknown. it must be processed first", name, parentName) return ErrParentUnknown } layer.Parent = &parent } } else { // The layer is already in the database, check if we need to update it. if layer.EngineVersion >= Version { log.Debugf(`layer %s: layer content has already been processed in the past with engine %d. Current engine is %d. skipping analysis`, name, layer.EngineVersion, Version) return nil } log.Debugf(`layer %s: layer content has been analyzed in the past with engine %d. Current engine is %d. analyzing again`, name, layer.EngineVersion, Version) } // Analyze the content. layer.Namespace, layer.Features, err = detectContent(imageFormat, name, path, headers, layer.Parent) if err != nil { return err } return datastore.InsertLayer(layer) }
// FetchUpdate gets vulnerability updates from the Red Hat OVAL definitions. func (f *RHELFetcher) FetchUpdate(datastore database.Datastore) (resp updater.FetcherResponse, err error) { log.Info("fetching Red Hat vulnerabilities") // Get the first RHSA we have to manage. flagValue, err := datastore.GetKeyValue(updaterFlag) if err != nil { return resp, err } firstRHSA, err := strconv.Atoi(flagValue) if firstRHSA == 0 || err != nil { firstRHSA = firstRHEL5RHSA } // Fetch the update list. r, err := http.Get(ovalURI) if err != nil { log.Errorf("could not download RHEL's update list: %s", err) return resp, cerrors.ErrCouldNotDownload } // Get the list of RHSAs that we have to process. var rhsaList []int scanner := bufio.NewScanner(r.Body) for scanner.Scan() { line := scanner.Text() r := rhsaRegexp.FindStringSubmatch(line) if len(r) == 2 { rhsaNo, _ := strconv.Atoi(r[1]) if rhsaNo > firstRHSA { rhsaList = append(rhsaList, rhsaNo) } } } for _, rhsa := range rhsaList { // Download the RHSA's XML file. r, err := http.Get(ovalURI + rhsaFilePrefix + strconv.Itoa(rhsa) + ".xml") if err != nil { log.Errorf("could not download RHEL's update file: %s", err) return resp, cerrors.ErrCouldNotDownload } // Parse the XML. vs, err := parseRHSA(r.Body) if err != nil { return resp, err } // Collect vulnerabilities. for _, v := range vs { resp.Vulnerabilities = append(resp.Vulnerabilities, v) } } // Set the flag if we found anything. if len(rhsaList) > 0 { resp.FlagName = updaterFlag resp.FlagValue = strconv.Itoa(rhsaList[len(rhsaList)-1]) } else { log.Debug("no Red Hat update.") } return resp, nil }
// FetchUpdate gets vulnerability updates from the Ubuntu CVE Tracker. func (fetcher *UbuntuFetcher) FetchUpdate(datastore database.Datastore) (resp updater.FetcherResponse, err error) { log.Info("fetching Ubuntu vulnerabilities") // Check to see if the repository does not already exist. if _, pathExists := os.Stat(fetcher.repositoryLocalPath); fetcher.repositoryLocalPath == "" || os.IsNotExist(pathExists) { // Create a temporary folder and download the repository. p, err := ioutil.TempDir(os.TempDir(), "ubuntu-cve-tracker") if err != nil { return resp, ErrFilesystem } // bzr wants an empty target directory. fetcher.repositoryLocalPath = p + "/repository" // Create the new repository. err = createRepository(fetcher.repositoryLocalPath) if err != nil { return resp, err } } else { // Update the repository that's already on disk. err = updateRepository(fetcher.repositoryLocalPath) if err != nil { return resp, err } } // Get revision number. revisionNumber, err := getRevisionNumber(fetcher.repositoryLocalPath) if err != nil { return resp, err } // Get the latest revision number we successfully applied in the database. dbRevisionNumber, err := datastore.GetKeyValue("ubuntuUpdater") if err != nil { return resp, err } // Get the list of vulnerabilities that we have to update. modifiedCVE, err := collectModifiedVulnerabilities(revisionNumber, dbRevisionNumber, fetcher.repositoryLocalPath) if err != nil { return resp, err } notes := make(map[string]struct{}) for cvePath := range modifiedCVE { // Open the CVE file. file, err := os.Open(fetcher.repositoryLocalPath + "/" + cvePath) if err != nil { // This can happen when a file is modified and then moved in another // commit. continue } // Parse the vulnerability. v, unknownReleases, err := parseUbuntuCVE(file) if err != nil { return resp, err } // Add the vulnerability to the response. resp.Vulnerabilities = append(resp.Vulnerabilities, v) // Store any unknown releases as notes. for k := range unknownReleases { note := fmt.Sprintf("Ubuntu %s is not mapped to any version number (eg. trusty->14.04). Please update me.", k) notes[note] = struct{}{} // If we encountered unknown Ubuntu release, we don't want the revision // number to be considered as managed. dbRevisionNumberInt, _ := strconv.Atoi(dbRevisionNumber) revisionNumber = dbRevisionNumberInt } // Close the file manually. // // We do that instead of using defer because defer works on a function-level scope. // We would open many files and close them all at once at the end of the function, // which could lead to exceed fs.file-max. file.Close() } // Add flag and notes. resp.FlagName = updaterFlag resp.FlagValue = strconv.Itoa(revisionNumber) for note := range notes { resp.Notes = append(resp.Notes, note) } return }
// Run starts the Notifier service. func Run(config *config.NotifierConfig, datastore database.Datastore, stopper *utils.Stopper) { defer stopper.End() // Configure registered notifiers. for notifierName, notifier := range notifiers { if configured, err := notifier.Configure(config); configured { log.Infof("notifier '%s' configured\n", notifierName) } else { delete(notifiers, notifierName) if err != nil { log.Errorf("could not configure notifier '%s': %s", notifierName, err) } } } // Do not run the updater if there is no notifier enabled. if len(notifiers) == 0 { log.Infof("notifier service is disabled") return } whoAmI := uuid.New() log.Infof("notifier service started. lock identifier: %s\n", whoAmI) for running := true; running; { // Find task. notification := findTask(datastore, config.RenotifyInterval, whoAmI, stopper) if notification == nil { // Interrupted while finding a task, Clair is stopping. break } // Handle task. done := make(chan bool, 1) go func() { success, interrupted := handleTask(*notification, stopper, config.Attempts) if success { utils.PrometheusObserveTimeMilliseconds(promNotifierLatencyMilliseconds, notification.Created) datastore.SetNotificationNotified(notification.Name) } if interrupted { running = false } datastore.Unlock(notification.Name, whoAmI) done <- true }() // Refresh task lock until done. outer: for { select { case <-done: break outer case <-time.After(refreshLockDuration): datastore.Lock(notification.Name, whoAmI, lockDuration, true) } } } log.Info("notifier service stopped") }
func testInsertLayerUpdate(t *testing.T, datastore database.Datastore) { f7 := database.FeatureVersion{ Feature: database.Feature{ Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"}, Name: "TestInsertLayerFeature7", }, Version: types.NewVersionUnsafe("0.01"), } l3, _ := datastore.FindLayer("TestInsertLayer3", true, false) l3u := database.Layer{ Name: l3.Name, Parent: l3.Parent, Namespace: &database.Namespace{Name: "TestInsertLayerNamespaceUpdated1"}, 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 testInsertLayerTree(t *testing.T, datastore database.Datastore) { f1 := database.FeatureVersion{ Feature: database.Feature{ Namespace: database.Namespace{Name: "TestInsertLayerNamespace2"}, Name: "TestInsertLayerFeature1", }, Version: types.NewVersionUnsafe("1.0"), } f2 := database.FeatureVersion{ Feature: database.Feature{ Namespace: database.Namespace{Name: "TestInsertLayerNamespace2"}, Name: "TestInsertLayerFeature2", }, Version: types.NewVersionUnsafe("0.34"), } f3 := database.FeatureVersion{ Feature: database.Feature{ Namespace: database.Namespace{Name: "TestInsertLayerNamespace2"}, Name: "TestInsertLayerFeature3", }, Version: types.NewVersionUnsafe("0.56"), } f4 := database.FeatureVersion{ Feature: database.Feature{ Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"}, Name: "TestInsertLayerFeature2", }, Version: types.NewVersionUnsafe("0.34"), } f5 := database.FeatureVersion{ Feature: database.Feature{ Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"}, Name: "TestInsertLayerFeature3", }, Version: types.NewVersionUnsafe("0.56"), } f6 := database.FeatureVersion{ Feature: database.Feature{ Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"}, Name: "TestInsertLayerFeature4", }, Version: types.NewVersionUnsafe("0.666"), } layers := []database.Layer{ { Name: "TestInsertLayer1", }, { Name: "TestInsertLayer2", Parent: &database.Layer{Name: "TestInsertLayer1"}, Namespace: &database.Namespace{Name: "TestInsertLayerNamespace1"}, }, // This layer changes the namespace and adds Features. { Name: "TestInsertLayer3", Parent: &database.Layer{Name: "TestInsertLayer2"}, Namespace: &database.Namespace{Name: "TestInsertLayerNamespace2"}, 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"}, 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)) } } }
// Run updates the vulnerability database at regular intervals. func Run(config *config.UpdaterConfig, datastore database.Datastore, st *utils.Stopper) { defer st.End() // Do not run the updater if there is no config or if the interval is 0. if config == nil || config.Interval == 0 { log.Infof("updater service is disabled.") return } whoAmI := uuid.New() log.Infof("updater service started. lock identifier: %s", whoAmI) for { var stop bool // Determine if this is the first update and define the next update time. // The next update time is (last update time + interval) or now if this is the first update. nextUpdate := time.Now().UTC() lastUpdate, firstUpdate, err := getLastUpdate(datastore) if err != nil { log.Errorf("an error occured while getting the last update time") nextUpdate = nextUpdate.Add(config.Interval) } else if firstUpdate == false { nextUpdate = lastUpdate.Add(config.Interval) } // If the next update timer is in the past, then try to update. if nextUpdate.Before(time.Now().UTC()) { // Attempt to get a lock on the the update. log.Debug("attempting to obtain update lock") hasLock, hasLockUntil := datastore.Lock(lockName, whoAmI, lockDuration, false) if hasLock { // Launch update in a new go routine. doneC := make(chan bool, 1) go func() { Update(datastore, firstUpdate) doneC <- true }() for done := false; !done && !stop; { select { case <-doneC: done = true case <-time.After(refreshLockDuration): // Refresh the lock until the update is done. datastore.Lock(lockName, whoAmI, lockDuration, true) case <-st.Chan(): stop = true } } // Unlock the update. datastore.Unlock(lockName, whoAmI) if stop { break } continue } else { lockOwner, lockExpiration, err := datastore.FindLock(lockName) if err != nil { log.Debug("update lock is already taken") nextUpdate = hasLockUntil } else { log.Debugf("update lock is already taken by %s until %v", lockOwner, lockExpiration) nextUpdate = lockExpiration } } } // Sleep, but remain stoppable until approximately the next update time. now := time.Now().UTC() waitUntil := nextUpdate.Add(time.Duration(rand.ExpFloat64()/0.5) * time.Second) log.Debugf("next update attempt scheduled for %v.", waitUntil) if !waitUntil.Before(now) { if !st.Sleep(waitUntil.Sub(time.Now())) { break } } } // Clean resources. for _, metadataFetcher := range metadataFetchers { metadataFetcher.Clean() } for _, fetcher := range fetchers { fetcher.Clean() } log.Info("updater service stopped") }
// FetchUpdate gets vulnerability updates from the Ubuntu CVE Tracker. func (fetcher *UbuntuFetcher) FetchUpdate(datastore database.Datastore) (resp updater.FetcherResponse, err error) { log.Info("fetching Ubuntu vulnerabilities") // Pull the bzr repository. if err = fetcher.pullRepository(); err != nil { return resp, err } // Get revision number. revisionNumber, err := getRevisionNumber(fetcher.repositoryLocalPath) if err != nil { return resp, err } // Get the latest revision number we successfully applied in the database. dbRevisionNumber, err := datastore.GetKeyValue("ubuntuUpdater") if err != nil { return resp, err } // Get the list of vulnerabilities that we have to update. modifiedCVE, err := collectModifiedVulnerabilities(revisionNumber, dbRevisionNumber, fetcher.repositoryLocalPath) if err != nil { return resp, err } notes := make(map[string]struct{}) for cvePath := range modifiedCVE { // Open the CVE file. file, err := os.Open(fetcher.repositoryLocalPath + "/" + cvePath) if err != nil { // This can happen when a file is modified and then moved in another // commit. continue } // Parse the vulnerability. v, unknownReleases, err := parseUbuntuCVE(file) if err != nil { return resp, err } // Add the vulnerability to the response. resp.Vulnerabilities = append(resp.Vulnerabilities, v) // Store any unknown releases as notes. for k := range unknownReleases { note := fmt.Sprintf("Ubuntu %s is not mapped to any version number (eg. trusty->14.04). Please update me.", k) notes[note] = struct{}{} // If we encountered unknown Ubuntu release, we don't want the revision // number to be considered as managed. dbRevisionNumberInt, _ := strconv.Atoi(dbRevisionNumber) revisionNumber = dbRevisionNumberInt } // Close the file manually. // // We do that instead of using defer because defer works on a function-level scope. // We would open many files and close them all at once at the end of the function, // which could lead to exceed fs.file-max. file.Close() } // Add flag and notes. resp.FlagName = updaterFlag resp.FlagValue = strconv.Itoa(revisionNumber) for note := range notes { resp.Notes = append(resp.Notes, note) } return }