2015-11-13 19:11:28 +00:00
// Copyright 2015 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2016-01-13 21:41:00 +00:00
package ubuntu
2015-11-13 19:11:28 +00:00
import (
"bufio"
"bytes"
2016-01-19 16:03:27 +00:00
"errors"
2015-11-13 19:11:28 +00:00
"fmt"
"io"
"io/ioutil"
"os"
"regexp"
"strconv"
"strings"
2016-12-28 01:45:11 +00:00
"github.com/coreos/pkg/capnslog"
2015-11-13 19:11:28 +00:00
"github.com/coreos/clair/database"
2016-12-28 01:45:11 +00:00
"github.com/coreos/clair/ext/versionfmt"
2015-11-13 19:11:28 +00:00
"github.com/coreos/clair/updater"
"github.com/coreos/clair/utils"
cerrors "github.com/coreos/clair/utils/errors"
"github.com/coreos/clair/utils/types"
)
const (
2016-02-08 18:37:52 +00:00
trackerURI = "https://launchpad.net/ubuntu-cve-tracker"
2016-05-25 21:29:03 +00:00
trackerRepository = "https://launchpad.net/ubuntu-cve-tracker"
2016-02-08 18:37:52 +00:00
updaterFlag = "ubuntuUpdater"
cveURL = "http://people.ubuntu.com/~ubuntu-security/cve/%s"
2015-11-13 19:11:28 +00:00
)
var (
ubuntuIgnoredReleases = map [ string ] struct { } {
2016-02-25 00:29:36 +00:00
"upstream" : { } ,
"devel" : { } ,
2015-11-13 19:11:28 +00:00
2016-02-25 00:29:36 +00:00
"dapper" : { } ,
"edgy" : { } ,
"feisty" : { } ,
"gutsy" : { } ,
"hardy" : { } ,
"intrepid" : { } ,
"jaunty" : { } ,
"karmic" : { } ,
"lucid" : { } ,
"maverick" : { } ,
"natty" : { } ,
"oneiric" : { } ,
"saucy" : { } ,
2015-11-13 19:11:28 +00:00
2016-02-25 00:29:36 +00:00
"vivid/ubuntu-core" : { } ,
"vivid/stable-phone-overlay" : { } ,
2016-01-25 17:58:51 +00:00
2015-11-13 19:11:28 +00:00
// Syntax error
2016-02-25 00:29:36 +00:00
"Patches" : { } ,
2015-11-13 19:11:28 +00:00
// Product
2016-02-25 00:29:36 +00:00
"product" : { } ,
2015-11-13 19:11:28 +00:00
}
affectsCaptureRegexp = regexp . MustCompile ( ` (?P<release>.*)_(?P<package>.*): (?P<status>[^\s]*)( \(+(?P<note>[^()]*)\)+)? ` )
affectsCaptureRegexpNames = affectsCaptureRegexp . SubexpNames ( )
2016-01-20 00:17:08 +00:00
log = capnslog . NewPackageLogger ( "github.com/coreos/clair" , "updater/fetchers/ubuntu" )
2016-01-19 16:03:27 +00:00
// ErrFilesystem is returned when a fetcher fails to interact with the local filesystem.
ErrFilesystem = errors . New ( "updater/fetchers: something went wrong when interacting with the fs" )
2015-11-13 19:11:28 +00:00
)
2016-01-29 16:22:54 +00:00
// UbuntuFetcher implements updater.Fetcher and gets vulnerability updates from
2015-11-13 19:11:28 +00:00
// the Ubuntu CVE Tracker.
2016-01-29 16:22:54 +00:00
type UbuntuFetcher struct {
repositoryLocalPath string
}
2015-11-13 19:11:28 +00:00
func init ( ) {
2016-01-19 16:03:27 +00:00
updater . RegisterFetcher ( "Ubuntu" , & UbuntuFetcher { } )
2015-11-13 19:11:28 +00:00
}
// FetchUpdate gets vulnerability updates from the Ubuntu CVE Tracker.
2016-01-20 00:17:08 +00:00
func ( fetcher * UbuntuFetcher ) FetchUpdate ( datastore database . Datastore ) ( resp updater . FetcherResponse , err error ) {
2016-02-23 17:07:41 +00:00
log . Info ( "fetching Ubuntu vulnerabilities" )
2015-11-13 19:11:28 +00:00
2016-05-25 19:41:12 +00:00
// Pull the bzr repository.
if err = fetcher . pullRepository ( ) ; err != nil {
return resp , err
2015-11-13 19:11:28 +00:00
}
2015-11-16 21:57:53 +00:00
// Get revision number.
2016-01-29 16:22:54 +00:00
revisionNumber , err := getRevisionNumber ( fetcher . repositoryLocalPath )
2015-11-16 21:57:53 +00:00
if err != nil {
return resp , err
}
2015-11-13 19:11:28 +00:00
// Get the latest revision number we successfully applied in the database.
2016-01-20 00:17:08 +00:00
dbRevisionNumber , err := datastore . GetKeyValue ( "ubuntuUpdater" )
2015-11-13 19:11:28 +00:00
if err != nil {
return resp , err
}
// Get the list of vulnerabilities that we have to update.
2016-01-29 16:22:54 +00:00
modifiedCVE , err := collectModifiedVulnerabilities ( revisionNumber , dbRevisionNumber , fetcher . repositoryLocalPath )
2015-11-13 19:11:28 +00:00
if err != nil {
return resp , err
}
2016-01-25 18:19:49 +00:00
notes := make ( map [ string ] struct { } )
2015-11-13 19:11:28 +00:00
for cvePath := range modifiedCVE {
2016-01-19 23:09:19 +00:00
// Open the CVE file.
2016-01-29 16:22:54 +00:00
file , err := os . Open ( fetcher . repositoryLocalPath + "/" + cvePath )
2015-11-13 19:11:28 +00:00
if err != nil {
// This can happen when a file is modified and then moved in another
// commit.
continue
}
2016-01-19 23:09:19 +00:00
// Parse the vulnerability.
2016-01-19 16:03:27 +00:00
v , unknownReleases , err := parseUbuntuCVE ( file )
2015-11-13 19:11:28 +00:00
if err != nil {
return resp , err
}
2016-01-25 19:50:48 +00:00
// Add the vulnerability to the response.
resp . Vulnerabilities = append ( resp . Vulnerabilities , v )
2015-11-13 19:11:28 +00:00
2016-01-25 18:19:49 +00:00
// Store any unknown releases as notes.
2015-11-13 19:11:28 +00:00
for k := range unknownReleases {
note := fmt . Sprintf ( "Ubuntu %s is not mapped to any version number (eg. trusty->14.04). Please update me." , k )
2016-01-25 18:19:49 +00:00
notes [ note ] = struct { } { }
2015-11-13 19:11:28 +00:00
// If we encountered unknown Ubuntu release, we don't want the revision
// number to be considered as managed.
dbRevisionNumberInt , _ := strconv . Atoi ( dbRevisionNumber )
revisionNumber = dbRevisionNumberInt
}
2015-12-16 20:30:03 +00:00
// 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 ( )
2015-11-13 19:11:28 +00:00
}
2016-01-25 18:19:49 +00:00
// Add flag and notes.
2016-02-08 18:37:52 +00:00
resp . FlagName = updaterFlag
2015-11-13 19:11:28 +00:00
resp . FlagValue = strconv . Itoa ( revisionNumber )
2016-01-25 18:19:49 +00:00
for note := range notes {
resp . Notes = append ( resp . Notes , note )
}
2015-11-13 19:11:28 +00:00
return
}
2016-05-25 19:41:12 +00:00
func ( fetcher * UbuntuFetcher ) pullRepository ( ) ( err error ) {
// Determine whether we should branch or pull.
if _ , pathExists := os . Stat ( fetcher . repositoryLocalPath ) ; fetcher . repositoryLocalPath == "" || os . IsNotExist ( pathExists ) {
// Create a temporary folder to store the repository.
if fetcher . repositoryLocalPath , err = ioutil . TempDir ( os . TempDir ( ) , "ubuntu-cve-tracker" ) ; err != nil {
return ErrFilesystem
}
// Branch repository.
if out , err := utils . Exec ( fetcher . repositoryLocalPath , "bzr" , "branch" , "--use-existing-dir" , trackerRepository , "." ) ; err != nil {
log . Errorf ( "could not branch Ubuntu repository: %s. output: %s" , err , out )
return cerrors . ErrCouldNotDownload
}
return nil
}
// Pull repository.
if out , err := utils . Exec ( fetcher . repositoryLocalPath , "bzr" , "pull" , "--overwrite" ) ; err != nil {
os . RemoveAll ( fetcher . repositoryLocalPath )
log . Errorf ( "could not pull Ubuntu repository: %s. output: %s" , err , out )
return cerrors . ErrCouldNotDownload
}
return nil
}
func getRevisionNumber ( pathToRepo string ) ( int , error ) {
out , err := utils . Exec ( pathToRepo , "bzr" , "revno" )
if err != nil {
log . Errorf ( "could not get Ubuntu repository's revision number: %s. output: %s" , err , out )
return 0 , cerrors . ErrCouldNotDownload
}
revno , err := strconv . Atoi ( strings . TrimSpace ( string ( out ) ) )
if err != nil {
log . Errorf ( "could not parse Ubuntu repository's revision number: %s. output: %s" , err , out )
return 0 , cerrors . ErrCouldNotDownload
}
return revno , nil
}
2015-11-13 19:11:28 +00:00
func collectModifiedVulnerabilities ( revision int , dbRevision , repositoryLocalPath string ) ( map [ string ] struct { } , error ) {
modifiedCVE := make ( map [ string ] struct { } )
// Handle a brand new database.
if dbRevision == "" {
for _ , folder := range [ ] string { "active" , "retired" } {
d , err := os . Open ( repositoryLocalPath + "/" + folder )
if err != nil {
log . Errorf ( "could not open Ubuntu vulnerabilities repository's folder: %s" , err )
return nil , ErrFilesystem
}
// Get the FileInfo of all the files in the directory.
names , err := d . Readdirnames ( - 1 )
if err != nil {
log . Errorf ( "could not read Ubuntu vulnerabilities repository's folder:: %s." , err )
return nil , ErrFilesystem
}
// Add the vulnerabilities to the list.
for _ , name := range names {
if strings . HasPrefix ( name , "CVE-" ) {
modifiedCVE [ folder + "/" + name ] = struct { } { }
}
}
2015-12-16 20:30:03 +00:00
// 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.
d . Close ( )
2015-11-13 19:11:28 +00:00
}
return modifiedCVE , nil
}
// Handle an up to date database.
dbRevisionInt , _ := strconv . Atoi ( dbRevision )
if revision == dbRevisionInt {
log . Debug ( "no Ubuntu update" )
return modifiedCVE , nil
}
// Handle a database that needs upgrading.
out , err := utils . Exec ( repositoryLocalPath , "bzr" , "log" , "--verbose" , "-r" + strconv . Itoa ( dbRevisionInt + 1 ) + ".." , "-n0" )
if err != nil {
2015-11-16 21:57:53 +00:00
log . Errorf ( "could not get Ubuntu vulnerabilities repository logs: %s. output: %s" , err , out )
2015-11-13 19:11:28 +00:00
return nil , cerrors . ErrCouldNotDownload
}
scanner := bufio . NewScanner ( bytes . NewReader ( out ) )
for scanner . Scan ( ) {
text := strings . TrimSpace ( scanner . Text ( ) )
if strings . Contains ( text , "CVE-" ) && ( strings . HasPrefix ( text , "active/" ) || strings . HasPrefix ( text , "retired/" ) ) {
if strings . Contains ( text , " => " ) {
text = text [ strings . Index ( text , " => " ) + 4 : ]
}
modifiedCVE [ text ] = struct { } { }
}
}
return modifiedCVE , nil
}
2016-01-19 16:03:27 +00:00
func parseUbuntuCVE ( fileContent io . Reader ) ( vulnerability database . Vulnerability , unknownReleases map [ string ] struct { } , err error ) {
2015-11-13 19:11:28 +00:00
unknownReleases = make ( map [ string ] struct { } )
readingDescription := false
scanner := bufio . NewScanner ( fileContent )
for scanner . Scan ( ) {
line := strings . TrimSpace ( scanner . Text ( ) )
// Skip any comments.
if strings . HasPrefix ( line , "#" ) {
continue
}
// Parse the name.
if strings . HasPrefix ( line , "Candidate:" ) {
2016-01-19 16:03:27 +00:00
vulnerability . Name = strings . TrimSpace ( strings . TrimPrefix ( line , "Candidate:" ) )
2016-02-08 18:37:52 +00:00
vulnerability . Link = fmt . Sprintf ( cveURL , vulnerability . Name )
2015-11-13 19:11:28 +00:00
continue
}
// Parse the priority.
if strings . HasPrefix ( line , "Priority:" ) {
priority := strings . TrimSpace ( strings . TrimPrefix ( line , "Priority:" ) )
// Handle syntax error: Priority: medium (heap-protector)
if strings . Contains ( priority , " " ) {
priority = priority [ : strings . Index ( priority , " " ) ]
}
2016-01-19 16:03:27 +00:00
vulnerability . Severity = ubuntuPriorityToSeverity ( priority )
2015-11-13 19:11:28 +00:00
continue
}
// Parse the description.
if strings . HasPrefix ( line , "Description:" ) {
readingDescription = true
vulnerability . Description = strings . TrimSpace ( strings . TrimPrefix ( line , "Description:" ) ) // In case there is a formatting error and the description starts on the same line
continue
}
if readingDescription {
if strings . HasPrefix ( line , "Ubuntu-Description:" ) || strings . HasPrefix ( line , "Notes:" ) || strings . HasPrefix ( line , "Bugs:" ) || strings . HasPrefix ( line , "Priority:" ) || strings . HasPrefix ( line , "Discovered-by:" ) || strings . HasPrefix ( line , "Assigned-to:" ) {
readingDescription = false
} else {
vulnerability . Description = vulnerability . Description + " " + line
continue
}
}
// Try to parse the package that the vulnerability affects.
affectsCaptureArr := affectsCaptureRegexp . FindAllStringSubmatch ( line , - 1 )
if len ( affectsCaptureArr ) > 0 {
affectsCapture := affectsCaptureArr [ 0 ]
md := map [ string ] string { }
for i , n := range affectsCapture {
md [ affectsCaptureRegexpNames [ i ] ] = strings . TrimSpace ( n )
}
// Ignore Linux kernels.
if strings . HasPrefix ( md [ "package" ] , "linux" ) {
continue
}
2016-01-19 16:03:27 +00:00
// Only consider the package if its status is needed, active, deferred, not-affected or
// released. Ignore DNE (package does not exist), needs-triage, ignored, pending.
if md [ "status" ] == "needed" || md [ "status" ] == "active" || md [ "status" ] == "deferred" || md [ "status" ] == "released" || md [ "status" ] == "not-affected" {
2015-11-13 19:11:28 +00:00
if _ , isReleaseIgnored := ubuntuIgnoredReleases [ md [ "release" ] ] ; isReleaseIgnored {
continue
}
if _ , isReleaseKnown := database . UbuntuReleasesMapping [ md [ "release" ] ] ; ! isReleaseKnown {
unknownReleases [ md [ "release" ] ] = struct { } { }
continue
}
2016-12-28 01:45:11 +00:00
var version string
2015-11-13 19:11:28 +00:00
if md [ "status" ] == "released" {
if md [ "note" ] != "" {
var err error
2016-12-28 01:45:11 +00:00
err = versionfmt . Valid ( "dpkg" , md [ "note" ] )
2015-11-13 19:11:28 +00:00
if err != nil {
log . Warningf ( "could not parse package version '%s': %s. skipping" , md [ "note" ] , err )
}
2016-12-28 01:45:11 +00:00
version = md [ "note" ]
2015-11-13 19:11:28 +00:00
}
2016-01-19 16:03:27 +00:00
} else if md [ "status" ] == "not-affected" {
2016-12-28 01:45:11 +00:00
version = versionfmt . MinVersion
2015-11-13 19:11:28 +00:00
} else {
2016-12-28 01:45:11 +00:00
version = versionfmt . MaxVersion
2015-11-13 19:11:28 +00:00
}
2016-12-28 01:45:11 +00:00
if version == "" {
2015-11-13 19:11:28 +00:00
continue
}
// Create and add the new package.
2016-01-19 16:03:27 +00:00
featureVersion := database . FeatureVersion {
Feature : database . Feature {
Namespace : database . Namespace { Name : "ubuntu:" + database . UbuntuReleasesMapping [ md [ "release" ] ] } ,
Name : md [ "package" ] ,
} ,
2015-12-01 19:58:17 +00:00
Version : version ,
}
2016-01-19 16:03:27 +00:00
vulnerability . FixedIn = append ( vulnerability . FixedIn , featureVersion )
2015-11-13 19:11:28 +00:00
}
}
}
// Trim extra spaces in the description
vulnerability . Description = strings . TrimSpace ( vulnerability . Description )
// If no link has been provided (CVE-2006-NNN0 for instance), add the link to the tracker
if vulnerability . Link == "" {
2016-02-08 18:37:52 +00:00
vulnerability . Link = trackerURI
2015-11-13 19:11:28 +00:00
}
// If no priority has been provided (CVE-2007-0667 for instance), set the priority to Unknown
2016-01-20 00:17:08 +00:00
if vulnerability . Severity == "" {
vulnerability . Severity = types . Unknown
2015-11-13 19:11:28 +00:00
}
return
}
2016-01-19 16:03:27 +00:00
func ubuntuPriorityToSeverity ( priority string ) types . Priority {
2015-11-13 19:11:28 +00:00
switch priority {
case "untriaged" :
return types . Unknown
case "negligible" :
return types . Negligible
case "low" :
return types . Low
case "medium" :
return types . Medium
case "high" :
return types . High
case "critical" :
return types . Critical
}
log . Warning ( "Could not determine a vulnerability priority from: %s" , priority )
return types . Unknown
}
2016-01-29 16:22:54 +00:00
// Clean deletes any allocated resources.
func ( fetcher * UbuntuFetcher ) Clean ( ) {
2016-05-25 19:41:12 +00:00
os . RemoveAll ( fetcher . repositoryLocalPath )
2016-01-29 16:22:54 +00:00
}