diff --git a/api/v1/README.md b/api/v1/README.md index d9ab6aceda..e69cb98497 100644 --- a/api/v1/README.md +++ b/api/v1/README.md @@ -144,7 +144,7 @@ Server: clair "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" + "FixedBy": ">= 9.23-5" } ] } @@ -289,7 +289,7 @@ POST http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities HTTP/1.1 { "Name": "coreutils", "NamespaceName": "debian:8", - "Version": "8.23-1" + "FixedInVersions": ">= 8.23-1" } ] } @@ -322,7 +322,7 @@ Server: clair { "Name": "coreutils", "NamespaceName": "debian:8", - "Version": "8.23-1" + "FixedInVersions": ">= 8.23-1" } ] } @@ -373,7 +373,7 @@ Server: clair { "Name": "coreutils", "NamespaceName": "debian:8", - "Version": "8.23-1" + "FixedInVersions": ">= 8.23-1" } ] } @@ -592,7 +592,7 @@ Server: clair { "Name": "grep", "NamespaceName": "debian:8", - "Version": "2.25" + "FixedInVersions": ">= 2.25" } ] }, diff --git a/api/v1/models.go b/api/v1/models.go index 0d6a638359..9424fb4963 100644 --- a/api/v1/models.go +++ b/api/v1/models.go @@ -77,7 +77,7 @@ func LayerFromDatabaseModel(dbLayer database.Layer, withFeatures, withVulnerabil Metadata: dbVuln.Metadata, } - if dbVuln.FixedBy != types.MaxVersion { + if dbVuln.FixedBy.String() != types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MaxVersion).String() { vuln.FixedBy = dbVuln.FixedBy.String() } feature.Vulnerabilities = append(feature.Vulnerabilities, vuln) @@ -154,31 +154,32 @@ type Feature struct { Name string `json:"Name,omitempty"` NamespaceName string `json:"NamespaceName,omitempty"` Version string `json:"Version,omitempty"` + FixedInVersions string `json:"FixedInVersions,omitempty"` Vulnerabilities []Vulnerability `json:"Vulnerabilities,omitempty"` AddedBy string `json:"AddedBy,omitempty"` } func FeatureFromDatabaseModel(dbFeatureVersion database.FeatureVersion) Feature { - versionStr := dbFeatureVersion.Version.String() - if versionStr == types.MaxVersion.String() { - versionStr = "None" + fixedInVersionsStr := dbFeatureVersion.FixedInVersions.String() + if fixedInVersionsStr == types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MaxVersion).String() { + fixedInVersionsStr = "None" } return Feature{ - Name: dbFeatureVersion.Feature.Name, - NamespaceName: dbFeatureVersion.Feature.Namespace.Name, - Version: versionStr, - AddedBy: dbFeatureVersion.AddedBy.Name, + Name: dbFeatureVersion.Feature.Name, + NamespaceName: dbFeatureVersion.Feature.Namespace.Name, + FixedInVersions: fixedInVersionsStr, + AddedBy: dbFeatureVersion.AddedBy.Name, } } func (f Feature) DatabaseModel() (database.FeatureVersion, error) { - var version types.Version - if f.Version == "None" { - version = types.MaxVersion + var fivs types.FixedInVersions + if f.FixedInVersions == "None" { + fivs = types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MaxVersion) } else { var err error - version, err = types.NewVersion(f.Version) + fivs, err = types.NewFixedInVersions(f.FixedInVersions) if err != nil { return database.FeatureVersion{}, err } @@ -189,7 +190,7 @@ func (f Feature) DatabaseModel() (database.FeatureVersion, error) { Name: f.Name, Namespace: database.Namespace{Name: f.NamespaceName}, }, - Version: version, + FixedInVersions: fivs, }, nil } diff --git a/cmd/clair/main.go b/cmd/clair/main.go index df3d0ba2be..5c1e2ed221 100644 --- a/cmd/clair/main.go +++ b/cmd/clair/main.go @@ -29,6 +29,7 @@ import ( _ "github.com/coreos/clair/notifier/notifiers" _ "github.com/coreos/clair/updater/fetchers/debian" + _ "github.com/coreos/clair/updater/fetchers/nodejs" _ "github.com/coreos/clair/updater/fetchers/rhel" _ "github.com/coreos/clair/updater/fetchers/ubuntu" _ "github.com/coreos/clair/updater/metadata_fetchers/nvd" @@ -37,10 +38,12 @@ import ( _ "github.com/coreos/clair/worker/detectors/data/docker" _ "github.com/coreos/clair/worker/detectors/feature/dpkg" + _ "github.com/coreos/clair/worker/detectors/feature/npm" _ "github.com/coreos/clair/worker/detectors/feature/rpm" _ "github.com/coreos/clair/worker/detectors/namespace/aptsources" _ "github.com/coreos/clair/worker/detectors/namespace/lsbrelease" + _ "github.com/coreos/clair/worker/detectors/namespace/nodejs" _ "github.com/coreos/clair/worker/detectors/namespace/osrelease" _ "github.com/coreos/clair/worker/detectors/namespace/redhatrelease" diff --git a/database/models.go b/database/models.go index a44291b8a2..e88d236013 100644 --- a/database/models.go +++ b/database/models.go @@ -53,9 +53,10 @@ type Feature struct { type FeatureVersion struct { Model - Feature Feature - Version types.Version - AffectedBy []Vulnerability + Feature Feature + Version types.Version + FixedInVersions types.FixedInVersions + AffectedBy []Vulnerability // For output purposes. Only make sense when the feature version is in the context of an image. AddedBy Layer @@ -78,7 +79,7 @@ type Vulnerability struct { // For output purposes. Only make sense when the vulnerability // is already about a specific Feature/FeatureVersion. - FixedBy types.Version `json:",omitempty"` + FixedBy types.FixedInVersions `json:",omitempty"` } type MetadataMap map[string]interface{} diff --git a/database/pgsql/complex_test.go b/database/pgsql/complex_test.go index 46ba504ab4..a475a99f22 100644 --- a/database/pgsql/complex_test.go +++ b/database/pgsql/complex_test.go @@ -85,8 +85,8 @@ func TestRaceAffects(t *testing.T) { Namespace: feature.Namespace, FixedIn: []database.FeatureVersion{ { - Feature: feature, - Version: types.NewVersionUnsafe(strconv.Itoa(version)), + Feature: feature, + FixedInVersions: types.NewFixedInVersionsUnsafe(">=" + strconv.Itoa(version)), }, }, Severity: types.Unknown, @@ -149,7 +149,9 @@ func TestRaceAffects(t *testing.T) { // Get expected affects. for i := numVulnerabilities; i > featureVersionVersion; i-- { for _, vulnerability := range vulnerabilities[i] { - expectedAffectedNames = append(expectedAffectedNames, vulnerability.Name) + if vulnerability.FixedIn[0].FixedInVersions.Affected(featureVersion.Version) { + expectedAffectedNames = append(expectedAffectedNames, vulnerability.Name) + } } } diff --git a/database/pgsql/feature.go b/database/pgsql/feature.go index a2f2abe873..679b7152a7 100644 --- a/database/pgsql/feature.go +++ b/database/pgsql/feature.go @@ -194,7 +194,7 @@ func (pgSQL *pgSQL) insertFeatureVersions(featureVersions []database.FeatureVers type vulnerabilityAffectsFeatureVersion struct { vulnerabilityID int fixedInID int - fixedInVersion types.Version + fixedInVersions types.FixedInVersions } func linkFeatureVersionToVulnerabilities(tx *sql.Tx, featureVersion database.FeatureVersion) error { @@ -210,14 +210,12 @@ func linkFeatureVersionToVulnerabilities(tx *sql.Tx, featureVersion database.Fea for rows.Next() { var affect vulnerabilityAffectsFeatureVersion - err := rows.Scan(&affect.fixedInID, &affect.vulnerabilityID, &affect.fixedInVersion) + err := rows.Scan(&affect.fixedInID, &affect.vulnerabilityID, &affect.fixedInVersions) if err != nil { return handleError("searchVulnerabilityFixedInFeature.Scan()", err) } - if featureVersion.Version.Compare(affect.fixedInVersion) < 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. + if affect.fixedInVersions.Affected(featureVersion.Version) { affects = append(affects, affect) } } diff --git a/database/pgsql/layer_test.go b/database/pgsql/layer_test.go index c45cbbed46..fc6c900fb9 100644 --- a/database/pgsql/layer_test.go +++ b/database/pgsql/layer_test.go @@ -93,7 +93,7 @@ func TestFindLayer(t *testing.T) { assert.Equal(t, types.High, 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, types.NewVersionUnsafe("2.0"), featureVersion.AffectedBy[0].FixedBy) + assert.Equal(t, types.NewFixedInVersionsUnsafe(">= 2.0").String(), featureVersion.AffectedBy[0].FixedBy.String()) } default: t.Errorf("unexpected package %s for layer-1", featureVersion.Feature.Name) diff --git a/database/pgsql/notification_test.go b/database/pgsql/notification_test.go index 3f90f34963..e8fb3705d7 100644 --- a/database/pgsql/notification_test.go +++ b/database/pgsql/notification_test.go @@ -104,8 +104,8 @@ func TestNotification(t *testing.T) { Severity: "Unknown", FixedIn: []database.FeatureVersion{ { - Feature: f1, - Version: types.NewVersionUnsafe("1.0"), + Feature: f1, + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 1.0"), }, }, } @@ -164,12 +164,12 @@ func TestNotification(t *testing.T) { v1b.Severity = types.High v1b.FixedIn = []database.FeatureVersion{ { - Feature: f1, - Version: types.MinVersion, + Feature: f1, + FixedInVersions: types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MinVersion), }, { - Feature: f2, - Version: types.MaxVersion, + Feature: f2, + FixedInVersions: types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MaxVersion), }, } diff --git a/database/pgsql/testdata/data.sql b/database/pgsql/testdata/data.sql index 7a48ef6400..e91e42e876 100644 --- a/database/pgsql/testdata/data.sql +++ b/database/pgsql/testdata/data.sql @@ -48,8 +48,8 @@ INSERT INTO vulnerability (id, namespace_id, name, description, link, severity) (2, 1, 'CVE-NOPE', 'A vulnerability affecting nothing', '', 'Unknown'); INSERT INTO vulnerability_fixedin_feature (id, vulnerability_id, feature_id, version) VALUES - (1, 1, 2, '2.0'), - (2, 1, 4, '1.9-abc'); + (1, 1, 2, '>= 2.0'), + (2, 1, 4, '>= 1.9-abc'); INSERT INTO vulnerability_affects_featureversion (id, vulnerability_id, featureversion_id, fixedin_id) VALUES (1, 1, 2, 1); -- CVE-OPENSSL-1-DEB7 affects Debian:7 OpenSSL 1.0 diff --git a/database/pgsql/vulnerability.go b/database/pgsql/vulnerability.go index 74ee98282b..21e9c7f520 100644 --- a/database/pgsql/vulnerability.go +++ b/database/pgsql/vulnerability.go @@ -140,11 +140,11 @@ func scanVulnerability(queryer Queryer, queryName string, vulnerabilityRow *sql. for rows.Next() { var featureVersionID zero.Int - var featureVersionVersion zero.String + var featureVersionFixedInVersions zero.String var featureVersionFeatureName zero.String err := rows.Scan( - &featureVersionVersion, + &featureVersionFixedInVersions, &featureVersionID, &featureVersionFeatureName, ) @@ -163,7 +163,7 @@ func scanVulnerability(queryer Queryer, queryName string, vulnerabilityRow *sql. Namespace: vulnerability.Namespace, Name: featureVersionFeatureName.String, }, - Version: types.NewVersionUnsafe(featureVersionVersion.String), + FixedInVersions: types.NewFixedInVersionsUnsafe(featureVersionFixedInVersions.String), } vulnerability.FixedIn = append(vulnerability.FixedIn, featureVersion) } @@ -274,7 +274,7 @@ func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability, on // for diffing existing vulnerabilities. var fixedIn []database.FeatureVersion for _, fv := range vulnerability.FixedIn { - if fv.Version != types.MinVersion { + if fv.FixedInVersions.String() != types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MinVersion).String() { fixedIn = append(fixedIn, fv) } } @@ -350,7 +350,7 @@ func applyFixedInDiff(currentList, diff []database.FeatureVersion) ([]database.F different := false for _, name := range addedNames { - if diffMap[name].Version == types.MinVersion { + if diffMap[name].FixedInVersions.String() == types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MinVersion).String() { // MinVersion only makes sense when a Feature is already fixed in some version, // in which case we would be in the "inBothNames". continue @@ -363,7 +363,7 @@ func applyFixedInDiff(currentList, diff []database.FeatureVersion) ([]database.F for _, name := range inBothNames { fv := diffMap[name] - if fv.Version == types.MinVersion { + if fv.FixedInVersions.String() == types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MinVersion).String() { // MinVersion means that the Feature doesn't affect the Vulnerability anymore. delete(currentMap, name) different = true @@ -438,7 +438,7 @@ func (pgSQL *pgSQL) insertVulnerabilityFixedInFeatureVersions(tx *sql.Tx, vulner err = tx.QueryRow( insertVulnerabilityFixedInFeature, vulnerabilityID, fv.Feature.ID, - &fv.Version, + &fv.FixedInVersions, ).Scan(&fixedInID) if err != nil { @@ -446,7 +446,7 @@ func (pgSQL *pgSQL) insertVulnerabilityFixedInFeatureVersions(tx *sql.Tx, vulner } // Insert Vulnerability_Affects_FeatureVersion. - err = linkVulnerabilityToFeatureVersions(tx, fixedInID, vulnerabilityID, fv.Feature.ID, fv.Version) + err = linkVulnerabilityToFeatureVersions(tx, fixedInID, vulnerabilityID, fv.Feature.ID, fv.FixedInVersions) if err != nil { return err } @@ -455,7 +455,7 @@ func (pgSQL *pgSQL) insertVulnerabilityFixedInFeatureVersions(tx *sql.Tx, vulner return nil } -func linkVulnerabilityToFeatureVersions(tx *sql.Tx, fixedInID, vulnerabilityID, featureID int, fixedInVersion types.Version) error { +func linkVulnerabilityToFeatureVersions(tx *sql.Tx, fixedInID, vulnerabilityID, featureID int, fixedInVersions types.FixedInVersions) error { // Find every FeatureVersions of the Feature that the vulnerability affects. // TODO(Quentin-M): LIMIT rows, err := tx.Query(searchFeatureVersionByFeature, featureID) @@ -472,10 +472,7 @@ func linkVulnerabilityToFeatureVersions(tx *sql.Tx, fixedInID, vulnerabilityID, if err != nil { return handleError("searchFeatureVersionByFeature.Scan()", err) } - - if affected.Version.Compare(fixedInVersion) < 0 { - // The version of the FeatureVersion is lower than the fixed version of this vulnerability, - // thus, this FeatureVersion is affected by it. + if fixedInVersions.Affected(affected.Version) { affecteds = append(affecteds, affected) } } @@ -527,7 +524,7 @@ func (pgSQL *pgSQL) DeleteVulnerabilityFix(vulnerabilityNamespace, vulnerability Name: vulnerabilityNamespace, }, }, - Version: types.MinVersion, + FixedInVersions: types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MinVersion), }, }, } diff --git a/database/pgsql/vulnerability_test.go b/database/pgsql/vulnerability_test.go index d20c2e35f4..de18258783 100644 --- a/database/pgsql/vulnerability_test.go +++ b/database/pgsql/vulnerability_test.go @@ -46,12 +46,12 @@ func TestFindVulnerability(t *testing.T) { Namespace: database.Namespace{Name: "debian:7"}, FixedIn: []database.FeatureVersion{ { - Feature: database.Feature{Name: "openssl"}, - Version: types.NewVersionUnsafe("2.0"), + Feature: database.Feature{Name: "openssl"}, + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 2.0"), }, { - Feature: database.Feature{Name: "libssl"}, - Version: types.NewVersionUnsafe("1.9-abc"), + Feature: database.Feature{Name: "libssl"}, + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 1.9-abc"), }, }, } @@ -114,50 +114,50 @@ func TestInsertVulnerability(t *testing.T) { Name: "TestInsertVulnerabilityFeatureVersion1", Namespace: n1, }, - Version: types.NewVersionUnsafe("1.0"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 1.0"), } f2 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion1", Namespace: n2, }, - Version: types.NewVersionUnsafe("1.0"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 1.0"), } f3 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion2", }, - Version: types.MaxVersion, + FixedInVersions: types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MaxVersion), } f4 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion2", }, - Version: types.NewVersionUnsafe("1.4"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 1.4"), } f5 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion3", }, - Version: types.NewVersionUnsafe("1.5"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 1.5"), } f6 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion4", }, - Version: types.NewVersionUnsafe("0.1"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 0.1"), } f7 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion5", }, - Version: types.MaxVersion, + FixedInVersions: types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MaxVersion), } f8 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion5", }, - Version: types.MinVersion, + FixedInVersions: types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MinVersion), } // Insert invalid vulnerabilities. diff --git a/updater/fetchers/debian/debian.go b/updater/fetchers/debian/debian.go index e21bd3f216..84e9e5a544 100644 --- a/updater/fetchers/debian/debian.go +++ b/updater/fetchers/debian/debian.go @@ -69,6 +69,7 @@ func (fetcher *DebianFetcher) FetchUpdate(datastore database.Datastore) (resp up log.Errorf("could not download Debian's update: %s", err) return resp, cerrors.ErrCouldNotDownload } + defer r.Body.Close() // Get the SHA-1 of the latest update's JSON data latestHash, err := datastore.GetKeyValue(updaterFlag) @@ -195,7 +196,7 @@ func parseDebianJSON(data *jsonData) (vulnerabilities []database.Vulnerability, Name: "debian:" + database.DebianReleasesMapping[releaseName], }, }, - Version: version, + FixedInVersions: types.NewFixedInVersionsFromOV(types.OpGreaterEqual, version), } vulnerability.FixedIn = append(vulnerability.FixedIn, pkg) diff --git a/updater/fetchers/debian/debian_test.go b/updater/fetchers/debian/debian_test.go index e092909de4..40cea819af 100644 --- a/updater/fetchers/debian/debian_test.go +++ b/updater/fetchers/debian/debian_test.go @@ -31,6 +31,7 @@ func TestDebianParser(t *testing.T) { // Test parsing testdata/fetcher_debian_test.json testFile, _ := os.Open(filepath.Join(filepath.Dir(filename)) + "/testdata/fetcher_debian_test.json") response, err := buildResponse(testFile, "") + defer testFile.Close() if assert.Nil(t, err) && assert.Len(t, response.Vulnerabilities, 3) { for _, vulnerability := range response.Vulnerabilities { if vulnerability.Name == "CVE-2015-1323" { @@ -44,7 +45,7 @@ func TestDebianParser(t *testing.T) { Namespace: database.Namespace{Name: "debian:8"}, Name: "aptdaemon", }, - Version: types.MaxVersion, + FixedInVersions: types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MaxVersion), }, { Feature: database.Feature{ @@ -52,7 +53,7 @@ func TestDebianParser(t *testing.T) { Name: "aptdaemon", }, - Version: types.NewVersionUnsafe("1.1.1+bzr982-1"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 1.1.1+bzr982-1"), }, } @@ -70,21 +71,21 @@ func TestDebianParser(t *testing.T) { Namespace: database.Namespace{Name: "debian:8"}, Name: "aptdaemon", }, - Version: types.NewVersionUnsafe("0.7.0"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 0.7.0"), }, { Feature: database.Feature{ Namespace: database.Namespace{Name: "debian:unstable"}, Name: "aptdaemon", }, - Version: types.NewVersionUnsafe("0.7.0"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 0.7.0"), }, { Feature: database.Feature{ Namespace: database.Namespace{Name: "debian:8"}, Name: "asterisk", }, - Version: types.NewVersionUnsafe("0.5.56"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 0.5.56"), }, } @@ -102,7 +103,7 @@ func TestDebianParser(t *testing.T) { Namespace: database.Namespace{Name: "debian:8"}, Name: "asterisk", }, - Version: types.MinVersion, + FixedInVersions: types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MinVersion), }, } diff --git a/updater/fetchers/nodejs/nodejs.go b/updater/fetchers/nodejs/nodejs.go new file mode 100644 index 0000000000..88dcdb9ea8 --- /dev/null +++ b/updater/fetchers/nodejs/nodejs.go @@ -0,0 +1,153 @@ +// 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 nodejs + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/coreos/clair/database" + "github.com/coreos/clair/updater" + cerrors "github.com/coreos/clair/utils/errors" + "github.com/coreos/clair/utils/types" + "github.com/coreos/pkg/capnslog" +) + +const ( + url = "https://api.nodesecurity.io/advisories" + cveURLPrefix = "http://cve.mitre.org/cgi-bin/cvename.cgi?name=" + updaterFlag = "nodejsUpdater" + defaultNodejsVersion = "all" +) + +var log = capnslog.NewPackageLogger("github.com/coreos/clair", "updater/fetchers/nodejs") + +type nodejsAdvisory struct { + ID int `json:"id"` + UpdatedAt string `json:"updated_at"` + ModuleName string `json:"module_name"` + CVES []string `json:"cves"` + PatchedVersions string `json:"patched_versions"` + Overview string `json:"overview"` + CvssScore float32 `json:"cvss_score"` +} + +type nodejsAdvisories struct { + Total int `json:"total"` + Count int `json:"count"` + Offset int `json:"offset"` + Results []nodejsAdvisory `json:"results"` +} + +// NodejsFetcher implements updater.Fetcher for the Node Security Project +// (https://nodesecurity.io). +type NodejsFetcher struct{} + +func init() { + updater.RegisterFetcher("nodejs", &NodejsFetcher{}) +} + +// FetchUpdate fetches vulnerability updates from the Node Security Project. +func (fetcher *NodejsFetcher) FetchUpdate(datastore database.Datastore) (resp updater.FetcherResponse, err error) { + log.Info("fetching Nodejs vulnerabilities") + + // Download JSON. + r, err := http.Get(url) + if err != nil { + log.Errorf("could not download Nodejs's update: %s", err) + return resp, cerrors.ErrCouldNotDownload + } + defer r.Body.Close() + + // Get the latest date of the latest update's JSON data + latestUpdate, err := datastore.GetKeyValue(updaterFlag) + if err != nil { + return resp, err + } + + // Unmarshal JSON. + var advisories nodejsAdvisories + if err = json.NewDecoder(r.Body).Decode(&advisories); err != nil { + log.Errorf("could not unmarshal Nodejs's JSON: %s", err) + return resp, cerrors.ErrCouldNotParse + } + + resp.Vulnerabilities, resp.FlagValue = parseNodejsAdvisories(advisories.Results, latestUpdate) + resp.FlagName = updaterFlag + + return resp, nil +} + +func parseNodejsAdvisories(advisories []nodejsAdvisory, latestUpdate string) (vulnerabilities []database.Vulnerability, newUpdated string) { + mvulnerabilities := make(map[string]*database.Vulnerability) + + for _, advisory := range advisories { + if latestUpdate >= advisory.UpdatedAt { + break + } + if advisory.UpdatedAt > newUpdated { + newUpdated = advisory.UpdatedAt + } + for _, vulnName := range advisory.CVES { + // Get or create the vulnerability. + vulnerability, vulnerabilityAlreadyExists := mvulnerabilities[vulnName] + if !vulnerabilityAlreadyExists { + vulnerability = &database.Vulnerability{ + Name: vulnName, + Link: cveURLPrefix + strings.TrimLeft(vulnName, "CVE-"), + Severity: types.Unknown, + Description: advisory.Overview, + } + } + + // Set the priority of the vulnerability. + // A vulnerability has one urgency per advisory it affects. + // The highest urgency should be the one set. + if urgency := types.ScoreToPriority(advisory.CvssScore); urgency.Compare(vulnerability.Severity) > 0 { + vulnerability.Severity = urgency + } + + // Create and add the feature version. + pkg := database.FeatureVersion{ + Feature: database.Feature{ + Name: advisory.ModuleName, + Namespace: database.Namespace{ + Name: "nodejs:" + defaultNodejsVersion, + }, + }, + } + if fivs, err := types.NewFixedInVersions(advisory.PatchedVersions); err == nil { + pkg.FixedInVersions = fivs + } else { + log.Warningf("could not parse nodejs patched version: '%s'.", err) + } + vulnerability.FixedIn = append(vulnerability.FixedIn, pkg) + + // Store the vulnerability. + mvulnerabilities[vulnName] = vulnerability + } + } + + // Convert the vulnerabilities map to a slice + for _, v := range mvulnerabilities { + vulnerabilities = append(vulnerabilities, *v) + } + + return +} + +// Clean deletes any allocated resources. +func (fetcher *NodejsFetcher) Clean() {} diff --git a/updater/fetchers/nodejs/nodejs_test.go b/updater/fetchers/nodejs/nodejs_test.go new file mode 100644 index 0000000000..7bca1049fc --- /dev/null +++ b/updater/fetchers/nodejs/nodejs_test.go @@ -0,0 +1,86 @@ +// 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 nodejs + +import ( + "encoding/json" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/coreos/clair/database" + "github.com/coreos/clair/utils/types" + "github.com/stretchr/testify/assert" +) + +func TestNodejsParser(t *testing.T) { + _, filename, _, _ := runtime.Caller(0) + testFile, _ := os.Open(filepath.Join(filepath.Dir(filename)) + "/testdata/fetcher_nodejs_test.json") + defer testFile.Close() + + var advisories nodejsAdvisories + json.NewDecoder(testFile).Decode(&advisories) + assert.Len(t, advisories.Results, 4) + + vulnerabilities, lastUpdated := parseNodejsAdvisories(advisories.Results, "") + assert.Len(t, vulnerabilities, 2) + assert.Equal(t, "2016-05-13T20:39:38+00:00", lastUpdated) + + for _, vulnerability := range vulnerabilities { + if vulnerability.Name == "CVE-2015-7294" { + assert.Equal(t, "http://cve.mitre.org/cgi-bin/cvename.cgi?name=2015-7294", vulnerability.Link) + assert.Equal(t, types.Medium, vulnerability.Severity) + assert.Equal(t, "ldapauth versions <= 2.2.4 are vulnerable to ldap injection through the username parameter.", vulnerability.Description) + expectedFeatureVersions := []database.FeatureVersion{ + { + Feature: database.Feature{ + Namespace: database.Namespace{Name: "nodejs:" + defaultNodejsVersion}, + Name: "ldapauth", + }, + FixedInVersions: types.NewFixedInVersionsUnsafe("> 2.2.4"), + }, + { + Feature: database.Feature{ + Namespace: database.Namespace{Name: "nodejs:" + defaultNodejsVersion}, + Name: "ldapauth-fork", + }, + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 2.3.3"), + }, + } + for _, expectedFeatureVersion := range expectedFeatureVersions { + assert.Contains(t, vulnerability.FixedIn, expectedFeatureVersion) + } + } else if vulnerability.Name == "CVE-2016-2515" { + assert.Equal(t, "http://cve.mitre.org/cgi-bin/cvename.cgi?name=2016-2515", vulnerability.Link) + assert.Equal(t, types.Medium, vulnerability.Severity) + assert.Equal(t, "Specifically crafted long headers or uris can cause a minor denial of service when using hawk versions less than 4.1.1.\n\n\"The Regular expression Denial of Service (ReDoS) is a Denial of Service attack, that exploits the fact that most Regular Expression implementations may reach extreme situations that cause them to work very slowly (exponentially related to input size). An attacker can then cause a program using a Regular Expression to enter these extreme situations and then hang for a very long time.\"\n\nUpdates:\n- Updated to include fix in 3.1.3 ", vulnerability.Description) + expectedFeatureVersions := []database.FeatureVersion{ + { + Feature: database.Feature{ + Namespace: database.Namespace{Name: "nodejs:" + defaultNodejsVersion}, + Name: "hawk", + }, + FixedInVersions: types.NewFixedInVersionsUnsafe(">=3.1.3 < 4.0.0 || >=4.1.1"), + }, + } + for _, expectedFeatureVersion := range expectedFeatureVersions { + assert.Contains(t, vulnerability.FixedIn, expectedFeatureVersion) + } + } + } + + return +} diff --git a/updater/fetchers/nodejs/testdata/fetcher_nodejs_test.json b/updater/fetchers/nodejs/testdata/fetcher_nodejs_test.json new file mode 100644 index 0000000000..1728436280 --- /dev/null +++ b/updater/fetchers/nodejs/testdata/fetcher_nodejs_test.json @@ -0,0 +1,105 @@ +{ + "Total": 4, + "Count": 4, + "Offset": 0, + "Results": [ + { + "ID": 19, + "Created_at": "2015-10-17T19:41:46.382+00:00", + "Updated_at": "2016-04-25T15:31:07+00:00", + "Publish_date": "2015-09-18T19:30:10+00:00", + "Title": "LDAP Injection", + "Author": "David Black, Jerome Touffe-Blin", + "Module_name": "ldapauth", + "CVES": [ + "CVE-2015-7294" + ], + "Vulnerable_versions": "<=2.2.4", + "Patched_versions": "> 2.2.4", + "Slug": "ldapauth_ldap-injection", + "Overview": "ldapauth versions <= 2.2.4 are vulnerable to ldap injection through the username parameter.", + "Recommandation": "", + "References": "- http://www.openwall.com/lists/oss-security/2015/09/18/4", + "Legacy_slug": "ldapauth-ldap-injection", + "Allowed_scopes": [ + "public", + "admin" + ], + "CVEs_vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N", + "Cvss_score": 5.3 + }, + { + "ID": 18, + "Created_at": "2015-10-17T19:41:46.382+00:00", + "Updated_at": "2016-04-28T16:50:25+00:00", + "Publish_date": "2015-09-18T19:29:10+00:00", + "Title": "LDAP Injection", + "Author": "Jerome Touffe-Blin", + "Module_name": "ldapauth-fork", + "CVES": [ + "CVE-2015-7294" + ], + "Vulnerable_versions": "< 2.3.3", + "Patched_versions": ">= 2.3.3", + "Slug": "ldapauth-fork_ldap-injection", + "Overview": "ldapauth-fork is a module forked from node-ldapauth and is used for ldap authentication \nThe username parameter is not filtered as per [LDAP Escape Specifications](https://tools.ietf.org/search/rfc4515#section-3) \nA malicious user is able to change their name to certain LDAP commands and run anything that they want.", + "Recommandation": "", + "References": "- https://github.com/vesse/node-ldapauth-fork/issues/21\n- https://github.com/vesse/node-ldapauth-fork/commit/3feea43e243698bcaeffa904a7324f4d96df60e4", + "Legacy_slug": "ldapauth-fork-ldap-injection", + "Allowed_scopes": [ + "public", + "admin" + ], + "CVEs_vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N", + "Cvss_score": 6.5 + }, + { + "ID": 99, + "Created_at": "2016-04-04T19:46:25+00:00", + "Updated_at": "2016-05-13T20:39:38+00:00", + "Publish_date": "2016-04-26T16:24:32+00:00", + "Title": "Insecure Defaults Allow MITM Over TLS", + "Author": "David Johansson", + "Module_name": "engine.io-client", + "CVES": [], + "Vulnerable_versions": "<= 1.6.8", + "Patched_versions": ">= 1.6.9", + "Slug": "engineio-client_tls-connections-over-websockets-vulnerable-to-mitm", + "Overview": "engine.io-client is the client for [engine.io](https://github.com/socketio/engine.io), the implementation of a transport-based cross-browser/cross-device bi-directional communication layer for Socket.IO.\n\nThe vulnerability is related to the way that node.js handles the `rejectUnauthorized` setting. If the value is something that evaluates to false, certificate verification will be disabled.\n\nThis is problematic as engine.io-client passes in an object for settings that includes the rejectUnauthorized property, whether it has been set or not. If the value has not been explicitly changed, it will be passed in as `null`, resulting in certificate verification being turned off:\n\n``` \n // line that causes bug\nthis.rejectUnauthorized = opts.rejectUnauthorized === undefined ? null : opts.rejectUnauthorized;\n ```", + "Recommandation": "", + "References": "- https://github.com/socketio/engine.io-client/commit/2c55b278a491bf45313ecc0825cf800e2f7ff5c1\n- https://www.cigital.com/blog/node-js-socket-io/", + "Legacy_slug": "", + "Allowed_scopes": [ + "admin", + "public" + ], + "CVEs_vector": "CVSS:3.0/AV:A/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N", + "Cvss_score": 6.8 + }, + { + "ID": 77, + "Created_at": "2016-01-19T21:50:30.175+00:00", + "Updated_at": "2016-04-21T00:16:58+00:00", + "Publish_date": "2016-01-19T21:51:35.396+00:00", + "Title": "Regular Expression Denial of Service", + "Author": "Adam Baldwin", + "Module_name": "hawk", + "CVES": [ + "CVE-2016-2515" + ], + "Vulnerable_versions": "< 3.1.3 || >= 4.0.0 < 4.1.1", + "Patched_versions": ">=3.1.3 < 4.0.0 || >=4.1.1", + "Slug": "hawk_regular-expression-denial-of-service", + "Overview": "Specifically crafted long headers or uris can cause a minor denial of service when using hawk versions less than 4.1.1.\n\n\"The Regular expression Denial of Service (ReDoS) is a Denial of Service attack, that exploits the fact that most Regular Expression implementations may reach extreme situations that cause them to work very slowly (exponentially related to input size). An attacker can then cause a program using a Regular Expression to enter these extreme situations and then hang for a very long time.\"\n\nUpdates:\n- Updated to include fix in 3.1.3 ", + "Recommandation": "", + "References": "- https://github.com/hueniverse/hawk/issues/168\n- https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS", + "Legacy_slug": "", + "Allowed_scopes": [ + "admin", + "public" + ], + "CVEs_vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L", + "Cvss_score": 5.3 + } + ] +} diff --git a/updater/fetchers/rhel/rhel.go b/updater/fetchers/rhel/rhel.go index de4072d12a..6f58995405 100644 --- a/updater/fetchers/rhel/rhel.go +++ b/updater/fetchers/rhel/rhel.go @@ -107,6 +107,7 @@ func (f *RHELFetcher) FetchUpdate(datastore database.Datastore) (resp updater.Fe log.Errorf("could not download RHEL's update list: %s", err) return resp, cerrors.ErrCouldNotDownload } + defer r.Body.Close() // Get the list of RHSAs that we have to process. var rhsaList []int @@ -282,11 +283,13 @@ func toFeatureVersions(criteria criteria) []database.FeatureVersion { } } else if strings.Contains(c.Comment, " is earlier than ") { const prefixLen = len(" is earlier than ") + var version types.Version featureVersion.Feature.Name = strings.TrimSpace(c.Comment[:strings.Index(c.Comment, " is earlier than ")]) - featureVersion.Version, err = types.NewVersion(c.Comment[strings.Index(c.Comment, " is earlier than ")+prefixLen:]) + version, err = types.NewVersion(c.Comment[strings.Index(c.Comment, " is earlier than ")+prefixLen:]) if err != nil { log.Warningf("could not parse package version '%s': %s. skipping", c.Comment[strings.Index(c.Comment, " is earlier than ")+prefixLen:], err.Error()) } + featureVersion.FixedInVersions = types.NewFixedInVersionsFromOV(types.OpGreaterEqual, version) } } @@ -296,7 +299,7 @@ func toFeatureVersions(criteria criteria) []database.FeatureVersion { continue } - if featureVersion.Feature.Namespace.Name != "" && featureVersion.Feature.Name != "" && featureVersion.Version.String() != "" { + if featureVersion.Feature.Namespace.Name != "" && featureVersion.Feature.Name != "" && featureVersion.FixedInVersions.String() != "" { featureVersionParameters[featureVersion.Feature.Namespace.Name+":"+featureVersion.Feature.Name] = featureVersion } else { log.Warningf("could not determine a valid package from criterions: %v", criterions) diff --git a/updater/fetchers/rhel/rhel_test.go b/updater/fetchers/rhel/rhel_test.go index 0e9ddbd007..7d6211676e 100644 --- a/updater/fetchers/rhel/rhel_test.go +++ b/updater/fetchers/rhel/rhel_test.go @@ -32,6 +32,7 @@ func TestRHELParser(t *testing.T) { // Test parsing testdata/fetcher_rhel_test.1.xml testFile, _ := os.Open(path + "/testdata/fetcher_rhel_test.1.xml") vulnerabilities, err := parseRHSA(testFile) + testFile.Close() if assert.Nil(t, err) && assert.Len(t, vulnerabilities, 1) { assert.Equal(t, "RHSA-2015:1193", vulnerabilities[0].Name) assert.Equal(t, "https://rhn.redhat.com/errata/RHSA-2015-1193.html", vulnerabilities[0].Link) @@ -44,21 +45,21 @@ func TestRHELParser(t *testing.T) { Namespace: database.Namespace{Name: "centos:7"}, Name: "xerces-c", }, - Version: types.NewVersionUnsafe("3.1.1-7.el7_1"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 3.1.1-7.el7_1"), }, { Feature: database.Feature{ Namespace: database.Namespace{Name: "centos:7"}, Name: "xerces-c-devel", }, - Version: types.NewVersionUnsafe("3.1.1-7.el7_1"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 3.1.1-7.el7_1"), }, { Feature: database.Feature{ Namespace: database.Namespace{Name: "centos:7"}, Name: "xerces-c-doc", }, - Version: types.NewVersionUnsafe("3.1.1-7.el7_1"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 3.1.1-7.el7_1"), }, } @@ -70,6 +71,7 @@ func TestRHELParser(t *testing.T) { // Test parsing testdata/fetcher_rhel_test.2.xml testFile, _ = os.Open(path + "/testdata/fetcher_rhel_test.2.xml") vulnerabilities, err = parseRHSA(testFile) + testFile.Close() if assert.Nil(t, err) && assert.Len(t, vulnerabilities, 1) { assert.Equal(t, "RHSA-2015:1207", vulnerabilities[0].Name) assert.Equal(t, "https://rhn.redhat.com/errata/RHSA-2015-1207.html", vulnerabilities[0].Link) @@ -82,14 +84,14 @@ func TestRHELParser(t *testing.T) { Namespace: database.Namespace{Name: "centos:6"}, Name: "firefox", }, - Version: types.NewVersionUnsafe("38.1.0-1.el6_6"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 38.1.0-1.el6_6"), }, { Feature: database.Feature{ Namespace: database.Namespace{Name: "centos:7"}, Name: "firefox", }, - Version: types.NewVersionUnsafe("38.1.0-1.el7_1"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 38.1.0-1.el7_1"), }, } diff --git a/updater/fetchers/ubuntu/ubuntu.go b/updater/fetchers/ubuntu/ubuntu.go index d54369062c..bfd9b92ce3 100644 --- a/updater/fetchers/ubuntu/ubuntu.go +++ b/updater/fetchers/ubuntu/ubuntu.go @@ -368,7 +368,7 @@ func parseUbuntuCVE(fileContent io.Reader) (vulnerability database.Vulnerability Namespace: database.Namespace{Name: "ubuntu:" + database.UbuntuReleasesMapping[md["release"]]}, Name: md["package"], }, - Version: version, + FixedInVersions: types.NewFixedInVersionsFromOV(types.OpGreaterEqual, version), } vulnerability.FixedIn = append(vulnerability.FixedIn, featureVersion) } diff --git a/updater/fetchers/ubuntu/ubuntu_test.go b/updater/fetchers/ubuntu/ubuntu_test.go index d76d457ed7..5a7aafef12 100644 --- a/updater/fetchers/ubuntu/ubuntu_test.go +++ b/updater/fetchers/ubuntu/ubuntu_test.go @@ -48,21 +48,21 @@ func TestUbuntuParser(t *testing.T) { Namespace: database.Namespace{Name: "ubuntu:14.04"}, Name: "libmspack", }, - Version: types.MaxVersion, + FixedInVersions: types.NewFixedInVersionsFromOV(types.OpGreaterEqual, types.MaxVersion), }, { Feature: database.Feature{ Namespace: database.Namespace{Name: "ubuntu:15.04"}, Name: "libmspack", }, - Version: types.NewVersionUnsafe("0.4-3"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 0.4-3"), }, { Feature: database.Feature{ Namespace: database.Namespace{Name: "ubuntu:15.10"}, Name: "libmspack-anotherpkg", }, - Version: types.NewVersionUnsafe("0.1"), + FixedInVersions: types.NewFixedInVersionsUnsafe(">= 0.1"), }, } diff --git a/utils/tar.go b/utils/tar.go index f2cff669b5..ff6a5a4918 100644 --- a/utils/tar.go +++ b/utils/tar.go @@ -24,6 +24,7 @@ import ( "io" "io/ioutil" "os/exec" + "regexp" "strings" ) @@ -94,7 +95,7 @@ func (r *TarReadCloser) Close() error { // SelectivelyExtractArchive extracts the specified files and folders // from targz data read from the given reader and store them in a map indexed by file paths -func SelectivelyExtractArchive(r io.Reader, prefix string, toExtract []string, maxFileSize int64) (map[string][]byte, error) { +func SelectivelyExtractArchive(r io.Reader, prefix string, toExtract []*regexp.Regexp, maxFileSize int64) (map[string][]byte, error) { data := make(map[string][]byte) // Create a tar or tar/tar-gzip/tar-bzip2/tar-xz reader @@ -123,8 +124,8 @@ func SelectivelyExtractArchive(r io.Reader, prefix string, toExtract []string, m // Determine if we should extract the element toBeExtracted := false - for _, s := range toExtract { - if strings.HasPrefix(filename, s) { + for _, re := range toExtract { + if re.MatchString(filename) { toBeExtracted = true break } diff --git a/utils/types/fixedin_versions.go b/utils/types/fixedin_versions.go new file mode 100644 index 0000000000..181d36cbfa --- /dev/null +++ b/utils/types/fixedin_versions.go @@ -0,0 +1,237 @@ +// 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 types + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "fmt" + "strings" + "unicode" +) + +type Operator string + +const ( + OpNotEqual Operator = "!=" + OpLessThan Operator = "<" + OpLessEqual Operator = "<=" + OpEqualTo Operator = "==" + OpGreaterEqual Operator = ">=" + OpGreaterThan Operator = ">" +) + +type FixedInVersions struct { + fivs [][]operVersion +} + +type operVersion struct { + oper Operator + version Version +} + +type ovState string + +const ( + ovStateInit ovState = "init" + ovStateOper ovState = "operation" + ovStateVersion ovState = "version" +) + +func isOperChar(ch rune) bool { + return ch == '>' || ch == '<' || ch == '=' +} + +func getOperator(str string) (oper Operator, error error) { + switch str { + case "!=": + case "<": + case "<=": + case "==": + case ">=": + case ">": + default: + return oper, fmt.Errorf("Invalid operator: '%s'", str) + } + + return Operator(str), nil +} + +func getFixedinVersion(content string) (ovs []operVersion, err error) { + state := ovStateInit + begin := 0 + var ov operVersion + for i, ch := range content { + if unicode.IsSpace(ch) { + continue + } + switch state { + case ovStateInit: + if isOperChar(ch) { + state = ovStateOper + } else { + // Default to '>=' + ov.oper = OpGreaterEqual + state = ovStateVersion + } + begin = i + case ovStateOper: + if !isOperChar(ch) { + state = ovStateVersion + if ov.oper, err = getOperator(strings.TrimSpace(content[begin:i])); err != nil { + return nil, err + } + begin = i + } + case ovStateVersion: + if isOperChar(ch) { + state = ovStateOper + if ov.version, err = NewVersion(strings.TrimSpace(content[begin:i])); err != nil { + return nil, err + } + ovs = append(ovs, ov) + begin = i + } + } + } + if state == ovStateVersion { + if ov.version, err = NewVersion(strings.TrimSpace(content[begin:len(content)])); err != nil { + return nil, err + } + ovs = append(ovs, ov) + } + + if len(ovs) == 0 { + err = fmt.Errorf("Failed to parse '%s'", content) + } + + return +} + +func (ov operVersion) patched(version Version) bool { + val := version.Compare(ov.version) + switch ov.oper { + case OpNotEqual: + return val != 0 + case OpLessThan: + return val < 0 + case OpLessEqual: + return val <= 0 + case OpEqualTo: + return val == 0 + case OpGreaterEqual: + return val >= 0 + case OpGreaterThan: + return val > 0 + } + + //Cannot get here + return false +} + +// String returns the string representation of a FixedInVersions +func (fivs FixedInVersions) String() (s string) { + firstFiv := false + for _, fiv := range fivs.fivs { + if !firstFiv { + firstFiv = true + } else { + s += " || " + } + + firstOV := false + for _, ov := range fiv { + if !firstOV { + firstOV = true + } else { + s += " " + } + s += string(ov.oper) + ov.version.String() + } + } + + return +} + +func (fivs FixedInVersions) MarshalJSON() ([]byte, error) { + return json.Marshal(fivs.String()) +} + +func (fivs *FixedInVersions) UnmarshalJSON(b []byte) (err error) { + var str string + json.Unmarshal(b, &str) + vp := NewFixedInVersionsUnsafe(str) + *fivs = vp + return +} + +func (fivs *FixedInVersions) Scan(value interface{}) (err error) { + val, ok := value.([]byte) + if !ok { + return errors.New("could not scan a Version from a non-string input") + } + *fivs, err = NewFixedInVersions(string(val)) + return +} + +func (fivs *FixedInVersions) Value() (driver.Value, error) { + return fivs.String(), nil +} + +func (fivs FixedInVersions) Affected(version Version) bool { + for _, fiv := range fivs.fivs { + affected := false + for _, ov := range fiv { + if !ov.patched(version) { + affected = true + break + } + } + if !affected { + return false + } + } + + return true +} + +func NewFixedInVersionsFromOV(oper Operator, version Version) FixedInVersions { + var fivs FixedInVersions + var fiv []operVersion + + fiv = append(fiv, operVersion{oper, version}) + fivs.fivs = append(fivs.fivs, fiv) + + return fivs +} + +func NewFixedInVersions(str string) (FixedInVersions, error) { + var fivs FixedInVersions + for _, ovsStr := range strings.Split(str, "||") { + if fiv, err := getFixedinVersion(ovsStr); err == nil { + fivs.fivs = append(fivs.fivs, fiv) + } else { + return fivs, err + } + } + + return fivs, nil +} + +func NewFixedInVersionsUnsafe(str string) FixedInVersions { + fivs, _ := NewFixedInVersions(str) + return fivs +} diff --git a/utils/types/fixedin_versions_test.go b/utils/types/fixedin_versions_test.go new file mode 100644 index 0000000000..0df3ba86ce --- /dev/null +++ b/utils/types/fixedin_versions_test.go @@ -0,0 +1,69 @@ +// 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 types + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFixedInVersions(t *testing.T) { + invalid_version := "3.1.3 < 4.0.0 || >= " + fivs, err := NewFixedInVersions(invalid_version) + assert.Error(t, err, "Failed to parse '%s'", ">=") + + invalid_version = "3.1.3 < ab.0.0 || >= " + fivs, err = NewFixedInVersions(invalid_version) + assert.Error(t, err, "Failed to parse '%s'", ">=") + + valid_version := "3.1.3" + fivs, err = NewFixedInVersions(valid_version) + assert.Nil(t, err) + + valid_version = ">=3.1.3 <4.0.0 || >=4.1.1" + fivs, err = NewFixedInVersions(valid_version) + assert.Nil(t, err) + assert.Equal(t, strings.Replace(fivs.String(), " ", "", -1), strings.Replace(valid_version, " ", "", -1)) + + for _, fiv := range fivs.fivs { + if len(fiv) == 1 { + assert.Equal(t, OpGreaterEqual, fiv[0].oper) + assert.Equal(t, NewVersionUnsafe("4.1.1"), fiv[0].version) + } else { + for _, ov := range fiv { + if ov.oper == OpGreaterEqual { + assert.Equal(t, NewVersionUnsafe("3.1.3"), ov.version) + } else if ov.oper == OpLessThan { + assert.Equal(t, NewVersionUnsafe("4.0.0"), ov.version) + } + } + } + } + + cases := []struct { + version string + expected bool + }{ + {"4.2", false}, + {"4.0.0", true}, + {"3.1.3", false}, + {"3.1.2", true}, + } + for _, c := range cases { + assert.Equal(t, fivs.Affected(NewVersionUnsafe(c.version)), c.expected) + } +} diff --git a/utils/types/priority.go b/utils/types/priority.go index aac56d40f4..66b493a530 100644 --- a/utils/types/priority.go +++ b/utils/types/priority.go @@ -108,3 +108,22 @@ func (p *Priority) Scan(value interface{}) error { func (p *Priority) Value() (driver.Value, error) { return string(*p), nil } + +// ScoreToPriority return a priority from a cvss score on the scale of 0 to 10. +func ScoreToPriority(score float32) Priority { + if score < 0.0 { + return Unknown + } else if score < 1.0 { + return Negligible + } else if score <= 3.9 { + return Low + } else if score <= 6.9 { + return Medium + } else if score <= 8.9 { + return High + } else if score <= 10.0 { + return Critical + } else { + return Unknown + } +} diff --git a/utils/types/priority_test.go b/utils/types/priority_test.go index 15558d5a86..30c55330d8 100644 --- a/utils/types/priority_test.go +++ b/utils/types/priority_test.go @@ -30,3 +30,13 @@ func TestIsValid(t *testing.T) { assert.False(t, Priority("Test").IsValid()) assert.True(t, Unknown.IsValid()) } + +func TestScoreToPriority(t *testing.T) { + assert.Equal(t, ScoreToPriority(-1.0), Unknown) + assert.Equal(t, ScoreToPriority(0.5), Negligible) + assert.Equal(t, ScoreToPriority(2.0), Low) + assert.Equal(t, ScoreToPriority(5.0), Medium) + assert.Equal(t, ScoreToPriority(7.0), High) + assert.Equal(t, ScoreToPriority(9.0), Critical) + assert.Equal(t, ScoreToPriority(12.0), Unknown) +} diff --git a/utils/utils_test.go b/utils/utils_test.go index 57bfc9e001..152e83d45b 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -18,6 +18,7 @@ import ( "bytes" "os" "path/filepath" + "regexp" "runtime" "testing" @@ -71,13 +72,13 @@ func TestTar(t *testing.T) { testArchivePath := filepath.Join(filepath.Dir(path), testDataDir, filename) // Extract non compressed data - data, err = SelectivelyExtractArchive(bytes.NewReader([]byte("that string does not represent a tar or tar-gzip file")), "", []string{}, 0) + data, err = SelectivelyExtractArchive(bytes.NewReader([]byte("that string does not represent a tar or tar-gzip file")), "", []*regexp.Regexp{}, 0) assert.Error(t, err, "Extracting non compressed data should return an error") // Extract an archive f, _ := os.Open(testArchivePath) defer f.Close() - data, err = SelectivelyExtractArchive(f, "", []string{"test/"}, 0) + data, err = SelectivelyExtractArchive(f, "", []*regexp.Regexp{regexp.MustCompile("^test/")}, 0) assert.Nil(t, err) if c, n := data["test/test.txt"]; !n { @@ -92,7 +93,7 @@ func TestTar(t *testing.T) { // File size limit f, _ = os.Open(testArchivePath) defer f.Close() - data, err = SelectivelyExtractArchive(f, "", []string{"test"}, 50) + data, err = SelectivelyExtractArchive(f, "", []*regexp.Regexp{regexp.MustCompile("test")}, 50) assert.Equal(t, ErrExtractedFileTooBig, err) } } diff --git a/worker/detectors/data.go b/worker/detectors/data.go index 376a254859..97d53a3b4e 100644 --- a/worker/detectors/data.go +++ b/worker/detectors/data.go @@ -22,6 +22,7 @@ import ( "math" "net/http" "os" + "regexp" "strings" "sync" @@ -34,7 +35,7 @@ type DataDetector interface { //Support check if the input path and format are supported by the underling detector Supported(path string, format string) bool // Detect detects the required data from input path - Detect(layerReader io.ReadCloser, toExtract []string, maxFileSize int64) (data map[string][]byte, err error) + Detect(layerReader io.ReadCloser, toExtract []*regexp.Regexp, maxFileSize int64) (data map[string][]byte, err error) } var ( @@ -70,7 +71,7 @@ func RegisterDataDetector(name string, f DataDetector) { } // DetectData finds the Data of the layer by using every registered DataDetector -func DetectData(format, path string, headers map[string]string, toExtract []string, maxFileSize int64) (data map[string][]byte, err error) { +func DetectData(format, path string, headers map[string]string, toExtract []*regexp.Regexp, maxFileSize int64) (data map[string][]byte, err error) { var layerReader io.ReadCloser if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { // Create a new HTTP request object. diff --git a/worker/detectors/data/aci/aci.go b/worker/detectors/data/aci/aci.go index 78551aa78f..a81735c673 100644 --- a/worker/detectors/data/aci/aci.go +++ b/worker/detectors/data/aci/aci.go @@ -16,6 +16,7 @@ package aci import ( "io" + "regexp" "strings" "github.com/coreos/clair/utils" @@ -36,6 +37,6 @@ func (detector *ACIDataDetector) Supported(path string, format string) bool { return false } -func (detector *ACIDataDetector) Detect(layerReader io.ReadCloser, toExtract []string, maxFileSize int64) (map[string][]byte, error) { +func (detector *ACIDataDetector) Detect(layerReader io.ReadCloser, toExtract []*regexp.Regexp, maxFileSize int64) (map[string][]byte, error) { return utils.SelectivelyExtractArchive(layerReader, "rootfs/", toExtract, maxFileSize) } diff --git a/worker/detectors/data/docker/docker.go b/worker/detectors/data/docker/docker.go index d70de3bc2f..6a442e7ff0 100644 --- a/worker/detectors/data/docker/docker.go +++ b/worker/detectors/data/docker/docker.go @@ -16,6 +16,7 @@ package docker import ( "io" + "regexp" "strings" "github.com/coreos/clair/utils" @@ -36,6 +37,6 @@ func (detector *DockerDataDetector) Supported(path string, format string) bool { return false } -func (detector *DockerDataDetector) Detect(layerReader io.ReadCloser, toExtract []string, maxFileSize int64) (map[string][]byte, error) { +func (detector *DockerDataDetector) Detect(layerReader io.ReadCloser, toExtract []*regexp.Regexp, maxFileSize int64) (map[string][]byte, error) { return utils.SelectivelyExtractArchive(layerReader, "", toExtract, maxFileSize) } diff --git a/worker/detectors/feature/dpkg/dpkg.go b/worker/detectors/feature/dpkg/dpkg.go index 6154d2cec4..c8475332a5 100644 --- a/worker/detectors/feature/dpkg/dpkg.go +++ b/worker/detectors/feature/dpkg/dpkg.go @@ -30,6 +30,7 @@ var ( dpkgSrcCaptureRegexp = regexp.MustCompile(`Source: (?P[^\s]*)( \((?P.*)\))?`) dpkgSrcCaptureRegexpNames = dpkgSrcCaptureRegexp.SubexpNames() + dpkgRegexp = regexp.MustCompile("^var/lib/dpkg/status$") ) // DpkgFeaturesDetector implements FeaturesDetector and detects dpkg packages @@ -110,6 +111,6 @@ func (detector *DpkgFeaturesDetector) Detect(data map[string][]byte) ([]database // GetRequiredFiles returns the list of files required for Detect, without // leading / -func (detector *DpkgFeaturesDetector) GetRequiredFiles() []string { - return []string{"var/lib/dpkg/status"} +func (detector *DpkgFeaturesDetector) GetRequiredFiles() []*regexp.Regexp { + return []*regexp.Regexp{dpkgRegexp} } diff --git a/worker/detectors/feature/npm/npm.go b/worker/detectors/feature/npm/npm.go new file mode 100644 index 0000000000..6f9c634888 --- /dev/null +++ b/worker/detectors/feature/npm/npm.go @@ -0,0 +1,89 @@ +// 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 npm + +import ( + "encoding/json" + "regexp" + + "github.com/coreos/clair/database" + "github.com/coreos/clair/utils/types" + "github.com/coreos/clair/worker/detectors" + "github.com/coreos/pkg/capnslog" +) + +var ( + log = capnslog.NewPackageLogger("github.com/coreos/clair", "npm") + + nodejsRegexp = regexp.MustCompile("(node_modules|nodejs)/.*/package.json$") +) + +// NpmFeaturesDetector implements FeaturesDetector and detects nodejs packages +type NpmFeaturesDetector struct{} + +type NodejsPkg struct { + Version string `json:"version"` + Name string `json:"name"` +} + +func init() { + detectors.RegisterFeaturesDetector("npm", &NpmFeaturesDetector{}) +} + +// Detect detects packages using *package.json from the input data +func (detector *NpmFeaturesDetector) Detect(data map[string][]byte) ([]database.FeatureVersion, error) { + // Create a map to store packages and ensure their uniqueness + packagesMap := make(map[string]database.FeatureVersion) + for filename, content := range data { + if !nodejsRegexp.MatchString(filename) { + continue + } + + var nodejsPkg NodejsPkg + err := json.Unmarshal(content, &nodejsPkg) + if err != nil { + log.Warningf("could not parse nodejs package file '%s': %s. skipping", filename, err.Error()) + continue + } + + version, err := types.NewVersion(nodejsPkg.Version) + if err != nil { + log.Warningf("could not parse package version '%s': %s. skipping", nodejsPkg.Version, err.Error()) + continue + } + + pkg := database.FeatureVersion{ + Feature: database.Feature{ + Name: nodejsPkg.Name, + }, + Version: version, + } + packagesMap[pkg.Feature.Name+"#"+pkg.Version.String()] = pkg + } + + // Convert the map to a slice + packages := make([]database.FeatureVersion, 0, len(packagesMap)) + for _, pkg := range packagesMap { + packages = append(packages, pkg) + } + + return packages, nil +} + +// GetRequiredFiles returns the list of files required for Detect, without +// leading / +func (detector *NpmFeaturesDetector) GetRequiredFiles() []*regexp.Regexp { + return []*regexp.Regexp{nodejsRegexp} +} diff --git a/worker/detectors/feature/npm/npm_test.go b/worker/detectors/feature/npm/npm_test.go new file mode 100644 index 0000000000..5d3ae97e70 --- /dev/null +++ b/worker/detectors/feature/npm/npm_test.go @@ -0,0 +1,46 @@ +// 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 npm + +import ( + "testing" + + "github.com/coreos/clair/database" + "github.com/coreos/clair/utils/types" + "github.com/coreos/clair/worker/detectors/feature" +) + +var npmPackagesTests = []feature.FeatureVersionTest{ + { + FeatureVersions: []database.FeatureVersion{ + { + Feature: database.Feature{Name: "npm"}, + Version: types.NewVersionUnsafe("1.3.10"), + }, + { + Feature: database.Feature{Name: "hawk"}, + Version: types.NewVersionUnsafe("4.0.1"), + }, + }, + Data: map[string][]byte{ + "usr/lib/nodejs/npm/package.json": feature.LoadFileForTest("npm/testdata/npm/package.json"), + "usr/local/lib/node_modules/hawk/package.json": feature.LoadFileForTest("npm/testdata/hawk/package.json"), + }, + }, +} + +func TestNpmFeaturesDetector(t *testing.T) { + feature.TestFeaturesDetector(t, &NpmFeaturesDetector{}, npmPackagesTests) +} diff --git a/worker/detectors/feature/npm/testdata/hawk/package.json b/worker/detectors/feature/npm/testdata/hawk/package.json new file mode 100755 index 0000000000..4c85025da7 --- /dev/null +++ b/worker/detectors/feature/npm/testdata/hawk/package.json @@ -0,0 +1,26 @@ +{ + "name": "hawk", + "description": "HTTP Hawk Authentication Scheme", + "version": "4.0.1", + "author": { + "name": "Eran Hammer", + "email": "eran@hammer.io", + "url": "http://hueniverse.com" + }, + "repository": { + "type": "git", + "url": "git://github.com/hueniverse/hawk" + }, + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "hoek": "3.x.x", + "boom": "3.x.x", + "cryptiles": "3.x.x", + "sntp": "2.x.x" + }, + "license": "BSD-3-Clause", + "_id": "hawk@4.1.2", + "_from": "hawk@" +} diff --git a/worker/detectors/feature/npm/testdata/npm/package.json b/worker/detectors/feature/npm/testdata/npm/package.json new file mode 100644 index 0000000000..ecf751cd83 --- /dev/null +++ b/worker/detectors/feature/npm/testdata/npm/package.json @@ -0,0 +1,19 @@ +{ + "version": "1.3.10", + "name": "npm", + "homepage": "https://npmjs.org/doc/", + "author": "Isaac Z. Schlueter (http://blog.izs.me)", + "repository": { + "type": "git", + "url": "https://github.com/isaacs/npm" + }, + "dependencies": { + "semver": "~2.1.0", + "github-url-from-git": "1.1.1" + }, + "engines": { + "node": ">=0.6", + "npm": "1" + }, + "license": "Artistic-2.0" +} diff --git a/worker/detectors/feature/rpm/rpm.go b/worker/detectors/feature/rpm/rpm.go index b501263905..4016e4c805 100644 --- a/worker/detectors/feature/rpm/rpm.go +++ b/worker/detectors/feature/rpm/rpm.go @@ -18,6 +18,7 @@ import ( "bufio" "io/ioutil" "os" + "regexp" "strings" "github.com/coreos/clair/database" @@ -28,7 +29,10 @@ import ( "github.com/coreos/pkg/capnslog" ) -var log = capnslog.NewPackageLogger("github.com/coreos/clair", "rpm") +var ( + log = capnslog.NewPackageLogger("github.com/coreos/clair", "rpm") + rpmRegexp = regexp.MustCompile("^var/lib/rpm/Packages$") +) // RpmFeaturesDetector implements FeaturesDetector and detects rpm packages // It requires the "rpm" binary to be in the PATH @@ -115,6 +119,6 @@ func (detector *RpmFeaturesDetector) Detect(data map[string][]byte) ([]database. // GetRequiredFiles returns the list of files required for Detect, without // leading / -func (detector *RpmFeaturesDetector) GetRequiredFiles() []string { - return []string{"var/lib/rpm/Packages"} +func (detector *RpmFeaturesDetector) GetRequiredFiles() []*regexp.Regexp { + return []*regexp.Regexp{rpmRegexp} } diff --git a/worker/detectors/features.go b/worker/detectors/features.go index da8d8c062d..02f039ef73 100644 --- a/worker/detectors/features.go +++ b/worker/detectors/features.go @@ -16,6 +16,7 @@ package detectors import ( "fmt" + "regexp" "sync" "github.com/coreos/clair/database" @@ -27,7 +28,7 @@ type FeaturesDetector interface { Detect(map[string][]byte) ([]database.FeatureVersion, error) // GetRequiredFiles returns the list of files required for Detect, without // leading /. - GetRequiredFiles() []string + GetRequiredFiles() []*regexp.Regexp } var ( @@ -70,7 +71,7 @@ func DetectFeatures(data map[string][]byte) ([]database.FeatureVersion, error) { // GetRequiredFilesFeatures returns the list of files required for Detect for every // registered FeaturesDetector, without leading /. -func GetRequiredFilesFeatures() (files []string) { +func GetRequiredFilesFeatures() (files []*regexp.Regexp) { for _, detector := range featuresDetectors { files = append(files, detector.GetRequiredFiles()...) } diff --git a/worker/detectors/namespace.go b/worker/detectors/namespace.go index 7d00cdfc97..e894b4b723 100644 --- a/worker/detectors/namespace.go +++ b/worker/detectors/namespace.go @@ -18,6 +18,7 @@ package detectors import ( "fmt" + "regexp" "sync" "github.com/coreos/clair/database" @@ -30,7 +31,7 @@ type NamespaceDetector interface { Detect(map[string][]byte) *database.Namespace // GetRequiredFiles returns the list of files required for Detect, without // leading /. - GetRequiredFiles() []string + GetRequiredFiles() []*regexp.Regexp } var ( @@ -73,7 +74,7 @@ func DetectNamespace(data map[string][]byte) *database.Namespace { // GetRequiredFilesNamespace returns the list of files required for DetectNamespace for every // registered NamespaceDetector, without leading /. -func GetRequiredFilesNamespace() (files []string) { +func GetRequiredFilesNamespace() (files []*regexp.Regexp) { for _, detector := range namespaceDetectors { files = append(files, detector.GetRequiredFiles()...) } diff --git a/worker/detectors/namespace/aptsources/aptsources.go b/worker/detectors/namespace/aptsources/aptsources.go index 69b6e30f69..56b3fc5936 100644 --- a/worker/detectors/namespace/aptsources/aptsources.go +++ b/worker/detectors/namespace/aptsources/aptsources.go @@ -16,12 +16,17 @@ package aptsources import ( "bufio" + "regexp" "strings" "github.com/coreos/clair/database" "github.com/coreos/clair/worker/detectors" ) +var ( + aptNSRegexp = regexp.MustCompile("^etc/apt/sources.list$") +) + // AptSourcesNamespaceDetector implements NamespaceDetector and detects the Namespace from the // /etc/apt/sources.list file. // @@ -80,6 +85,6 @@ func (detector *AptSourcesNamespaceDetector) Detect(data map[string][]byte) *dat return nil } -func (detector *AptSourcesNamespaceDetector) GetRequiredFiles() []string { - return []string{"etc/apt/sources.list"} +func (detector *AptSourcesNamespaceDetector) GetRequiredFiles() []*regexp.Regexp { + return []*regexp.Regexp{aptNSRegexp} } diff --git a/worker/detectors/namespace/lsbrelease/lsbrelease.go b/worker/detectors/namespace/lsbrelease/lsbrelease.go index eab1998468..b66f03ce8b 100644 --- a/worker/detectors/namespace/lsbrelease/lsbrelease.go +++ b/worker/detectors/namespace/lsbrelease/lsbrelease.go @@ -26,6 +26,8 @@ import ( var ( lsbReleaseOSRegexp = regexp.MustCompile(`^DISTRIB_ID=(.*)`) lsbReleaseVersionRegexp = regexp.MustCompile(`^DISTRIB_RELEASE=(.*)`) + + lsbNSRegexp = regexp.MustCompile("^etc/lsb-release$") ) // AptSourcesNamespaceDetector implements NamespaceDetector and detects the Namespace from the @@ -76,6 +78,6 @@ func (detector *LsbReleaseNamespaceDetector) Detect(data map[string][]byte) *dat } // GetRequiredFiles returns the list of files that are required for Detect() -func (detector *LsbReleaseNamespaceDetector) GetRequiredFiles() []string { - return []string{"etc/lsb-release"} +func (detector *LsbReleaseNamespaceDetector) GetRequiredFiles() []*regexp.Regexp { + return []*regexp.Regexp{lsbNSRegexp} } diff --git a/worker/detectors/namespace/nodejs/nodejs.go b/worker/detectors/namespace/nodejs/nodejs.go new file mode 100644 index 0000000000..388fae652e --- /dev/null +++ b/worker/detectors/namespace/nodejs/nodejs.go @@ -0,0 +1,123 @@ +// 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 nodejs + +import ( + "bufio" + "io/ioutil" + "os" + "regexp" + "strings" + + "github.com/coreos/clair/database" + "github.com/coreos/clair/utils" + "github.com/coreos/clair/worker/detectors" + "github.com/coreos/pkg/capnslog" +) + +var ( + log = capnslog.NewPackageLogger("github.com/coreos/clair", "worker/detectors/packages") + + defaultNodejsVersion = "all" + nodejsPkg = "nodejs" + nodejsNSRegexp = regexp.MustCompile(`bin/(node|nodejs|npm)$`) + nodejsNSRpmRegexp = regexp.MustCompile(`^var/lib/rpm/Packages$`) + nodejsNSDpkgRegexp = regexp.MustCompile(`^var/lib/dpkg/status$`) +) + +// NodejsNamespaceDetector implements NamespaceDetector and detects Nodejs +// from /var/lib/dpkg/status, /var/lib/rpm/Packages or node binary installed manually +type NodejsNamespaceDetector struct{} + +func init() { + detectors.RegisterNamespaceDetector("nodejs", &NodejsNamespaceDetector{}) + +} + +func (detector *NodejsNamespaceDetector) Detect(data map[string][]byte) *database.Namespace { + if ns := detectDpkgNodejs(data); ns != nil { + return ns + } else if ns := detectRpmNodejs(data); ns != nil { + return ns + } else if ns := detectNodejs(data); ns != nil { + return ns + } + + return nil +} + +func detectNodejs(data map[string][]byte) *database.Namespace { + for filename, _ := range data { + if nodejsNSRegexp.MatchString(filename) { + return &database.Namespace{Name: nodejsPkg + ":" + defaultNodejsVersion} + } + } + + return nil +} + +func detectDpkgNodejs(data map[string][]byte) *database.Namespace { + f, hasFile := data["var/lib/dpkg/status"] + if !hasFile { + return nil + } + + scanner := bufio.NewScanner(strings.NewReader(string(f))) + for scanner.Scan() { + line := scanner.Text() + + if strings.HasPrefix(line, "Package: ") { + if pkgName := strings.TrimSpace(strings.TrimPrefix(line, "Package: ")); pkgName == nodejsPkg { + return &database.Namespace{Name: nodejsPkg + ":" + defaultNodejsVersion} + } + } + } + + return nil +} + +func detectRpmNodejs(data map[string][]byte) *database.Namespace { + f, hasFile := data["var/lib/rpm/Packages"] + if !hasFile { + return nil + } + + // Write the required "Packages" file to disk + tmpDir, err := ioutil.TempDir(os.TempDir(), "rpm") + defer os.RemoveAll(tmpDir) + if err != nil { + log.Errorf("could not create temporary folder for RPM %s detection: %s", nodejsPkg, err) + return nil + } + + err = ioutil.WriteFile(tmpDir+"/Packages", f, 0700) + if err != nil { + log.Errorf("could not create temporary file for RPM %s detection: %s", nodejsPkg, err) + return nil + } + + out, err := utils.Exec(tmpDir, "rpm", "--dbpath", tmpDir, "-qi", nodejsPkg) + if err != nil { + log.Errorf("could not query RPM %s: %s. output: %s", nodejsPkg, err, string(out)) + return nil + } + + return &database.Namespace{Name: nodejsPkg + ":" + defaultNodejsVersion} +} + +// GetRequiredFiles returns the list of files that are required for Detect() +func (detector *NodejsNamespaceDetector) GetRequiredFiles() []*regexp.Regexp { + return []*regexp.Regexp{nodejsNSRegexp, nodejsNSRpmRegexp, nodejsNSDpkgRegexp} +} diff --git a/worker/detectors/namespace/nodejs/nodejs_test.go b/worker/detectors/namespace/nodejs/nodejs_test.go new file mode 100644 index 0000000000..3ca1aa0e70 --- /dev/null +++ b/worker/detectors/namespace/nodejs/nodejs_test.go @@ -0,0 +1,67 @@ +// 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 nodejs + +import ( + "io/ioutil" + "path/filepath" + "runtime" + "testing" + + "github.com/coreos/clair/database" + "github.com/coreos/clair/worker/detectors/namespace" +) + +var nodejsDkpgTests = []namespace.NamespaceTest{ + { + ExpectedNamespace: database.Namespace{Name: "nodejs:all"}, + Data: map[string][]byte{ + "var/lib/dpkg/status": []byte( + `Package: nodejs +Status: install ok installed +Priority: extra +Section: web +Installed-Size: 3043 +Maintainer: Ubuntu Developers +Architecture: amd64 +Version: 0.10.25 +Depends: libc-ares2 (>= 1.8.0), libc6 (>= 2.14), libssl1.0.0 (>= 1.0.1), libstdc++6 (>= 4.1.1), libv8-3.14.5, zlib1g (>= 1:1.1.4) +Homepage: http://nodejs.org/ +Original-Maintainer: Debian Javascript Maintainers `), + }, + }, + { + ExpectedNamespace: database.Namespace{Name: "nodejs:all"}, + Data: map[string][]byte{ + "var/lib/rpm/Packages": loadFileForTest("testdata/Packages"), + }, + }, + { + ExpectedNamespace: database.Namespace{Name: "nodejs:all"}, + Data: map[string][]byte{ + "usr/local/bin/node": []byte(""), + }, + }, +} + +func TestNodejsNamespaceDetector(t *testing.T) { + namespace.TestNamespaceDetector(t, &NodejsNamespaceDetector{}, nodejsDkpgTests) +} + +func loadFileForTest(name string) []byte { + _, filename, _, _ := runtime.Caller(0) + d, _ := ioutil.ReadFile(filepath.Join(filepath.Dir(filename), name)) + return d +} diff --git a/worker/detectors/namespace/nodejs/testdata/Packages b/worker/detectors/namespace/nodejs/testdata/Packages new file mode 100644 index 0000000000..4ad0cb3bc8 Binary files /dev/null and b/worker/detectors/namespace/nodejs/testdata/Packages differ diff --git a/worker/detectors/namespace/osrelease/osrelease.go b/worker/detectors/namespace/osrelease/osrelease.go index 118fb9fde3..6c2efc1c3f 100644 --- a/worker/detectors/namespace/osrelease/osrelease.go +++ b/worker/detectors/namespace/osrelease/osrelease.go @@ -26,6 +26,11 @@ import ( var ( osReleaseOSRegexp = regexp.MustCompile(`^ID=(.*)`) osReleaseVersionRegexp = regexp.MustCompile(`^VERSION_ID=(.*)`) + + osNSRegexps = []*regexp.Regexp{ + regexp.MustCompile("^etc/os-release$"), + regexp.MustCompile("^usr/lib/os-release$"), + } ) // OsReleaseNamespaceDetector implements NamespaceDetector and detects the OS from the @@ -42,7 +47,7 @@ func init() { func (detector *OsReleaseNamespaceDetector) Detect(data map[string][]byte) *database.Namespace { var OS, version string - for _, filePath := range detector.GetRequiredFiles() { + for _, filePath := range []string{"etc/os-release", "usr/lib/os-release"} { f, hasFile := data[filePath] if !hasFile { continue @@ -71,6 +76,6 @@ func (detector *OsReleaseNamespaceDetector) Detect(data map[string][]byte) *data } // GetRequiredFiles returns the list of files that are required for Detect() -func (detector *OsReleaseNamespaceDetector) GetRequiredFiles() []string { - return []string{"etc/os-release", "usr/lib/os-release"} +func (detector *OsReleaseNamespaceDetector) GetRequiredFiles() []*regexp.Regexp { + return osNSRegexps } diff --git a/worker/detectors/namespace/redhatrelease/redhatrelease.go b/worker/detectors/namespace/redhatrelease/redhatrelease.go index a6569b07d5..6d9ac5ef8f 100644 --- a/worker/detectors/namespace/redhatrelease/redhatrelease.go +++ b/worker/detectors/namespace/redhatrelease/redhatrelease.go @@ -22,7 +22,15 @@ import ( "github.com/coreos/clair/worker/detectors" ) -var redhatReleaseRegexp = regexp.MustCompile(`(?P[^\s]*) (Linux release|release) (?P[\d]+)`) +var ( + redhatReleaseRegexp = regexp.MustCompile(`(?P[^\s]*) (Linux release|release) (?P[\d]+)`) + + redhatNSRegexps = []*regexp.Regexp{ + regexp.MustCompile(`^etc/centos-release$`), + regexp.MustCompile(`^etc/redhat-release$`), + regexp.MustCompile(`^etc/system-release$`), + } +) // RedhatReleaseNamespaceDetector implements NamespaceDetector and detects the OS from the // /etc/centos-release, /etc/redhat-release and /etc/system-release files. @@ -38,7 +46,7 @@ func init() { } func (detector *RedhatReleaseNamespaceDetector) Detect(data map[string][]byte) *database.Namespace { - for _, filePath := range detector.GetRequiredFiles() { + for _, filePath := range []string{"etc/centos-release", "etc/redhat-release", "etc/system-release"} { f, hasFile := data[filePath] if !hasFile { continue @@ -54,6 +62,6 @@ func (detector *RedhatReleaseNamespaceDetector) Detect(data map[string][]byte) * } // GetRequiredFiles returns the list of files that are required for Detect() -func (detector *RedhatReleaseNamespaceDetector) GetRequiredFiles() []string { - return []string{"etc/centos-release", "etc/redhat-release", "etc/system-release"} +func (detector *RedhatReleaseNamespaceDetector) GetRequiredFiles() []*regexp.Regexp { + return redhatNSRegexps }