vulnsrc/ubuntu: use new git-based ubuntu tracker
This commit is contained in:
parent
c031f8ea0c
commit
456af5f48c
@ -18,14 +18,13 @@ package ubuntu
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@ -38,8 +37,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
trackerURI = "https://launchpad.net/ubuntu-cve-tracker"
|
trackerURI = "https://git.launchpad.net/ubuntu-cve-tracker"
|
||||||
trackerRepository = "https://launchpad.net/ubuntu-cve-tracker"
|
|
||||||
updaterFlag = "ubuntuUpdater"
|
updaterFlag = "ubuntuUpdater"
|
||||||
cveURL = "http://people.ubuntu.com/~ubuntu-security/cve/%s"
|
cveURL = "http://people.ubuntu.com/~ubuntu-security/cve/%s"
|
||||||
)
|
)
|
||||||
@ -74,6 +72,8 @@ var (
|
|||||||
|
|
||||||
affectsCaptureRegexp = regexp.MustCompile(`(?P<release>.*)_(?P<package>.*): (?P<status>[^\s]*)( \(+(?P<note>[^()]*)\)+)?`)
|
affectsCaptureRegexp = regexp.MustCompile(`(?P<release>.*)_(?P<package>.*): (?P<status>[^\s]*)( \(+(?P<note>[^()]*)\)+)?`)
|
||||||
affectsCaptureRegexpNames = affectsCaptureRegexp.SubexpNames()
|
affectsCaptureRegexpNames = affectsCaptureRegexp.SubexpNames()
|
||||||
|
|
||||||
|
errUnknownRelease = errors.New("found packages with CVEs for a verison of Ubuntu that Clair doesn't know about")
|
||||||
)
|
)
|
||||||
|
|
||||||
type updater struct {
|
type updater struct {
|
||||||
@ -84,211 +84,179 @@ func init() {
|
|||||||
vulnsrc.RegisterUpdater("ubuntu", &updater{})
|
vulnsrc.RegisterUpdater("ubuntu", &updater{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *updater) Update(datastore database.Datastore) (resp vulnsrc.UpdateResponse, err error) {
|
func (u *updater) Update(db database.Datastore) (resp vulnsrc.UpdateResponse, err error) {
|
||||||
log.WithField("package", "Ubuntu").Info("Start fetching vulnerabilities")
|
log.WithField("package", "Ubuntu").Info("Start fetching vulnerabilities")
|
||||||
|
|
||||||
// Pull the bzr repository.
|
// Pull the master branch.
|
||||||
if err = u.pullRepository(); err != nil {
|
var commit string
|
||||||
return resp, err
|
commit, err = u.pullRepository()
|
||||||
}
|
|
||||||
|
|
||||||
// Get revision number.
|
|
||||||
revisionNumber, err := getRevisionNumber(u.repositoryLocalPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := datastore.Begin()
|
// Open a database transaction.
|
||||||
|
tx, err := db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
// Get the latest revision number we successfully applied in the database.
|
// Ask the database for the latest commit we successfully applied.
|
||||||
dbRevisionNumber, ok, err := tx.FindKeyValue("ubuntuUpdater")
|
var dbCommit string
|
||||||
|
var ok bool
|
||||||
|
dbCommit, ok, err = tx.FindKeyValue(updaterFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Rollback(); err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
dbRevisionNumber = ""
|
dbCommit = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.WithField("package", "ubuntu").Debug("no update")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the list of vulnerabilities that we have to update.
|
// Get the list of vulnerabilities that we have to update.
|
||||||
modifiedCVE, err := collectModifiedVulnerabilities(revisionNumber, dbRevisionNumber, u.repositoryLocalPath)
|
var modifiedCVE map[string]struct{}
|
||||||
|
modifiedCVE, err = collectModifiedVulnerabilities(commit, dbCommit, u.repositoryLocalPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
notes := make(map[string]struct{})
|
// Get the list of vulnerabilities.
|
||||||
for cvePath := range modifiedCVE {
|
resp.Vulnerabilities, resp.Notes, err = collectVulnerabilitiesAndNotes(u.repositoryLocalPath, modifiedCVE)
|
||||||
// Open the CVE file.
|
|
||||||
file, err := os.Open(u.repositoryLocalPath + "/" + cvePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// This can happen when a file is modified and then moved in another
|
return
|
||||||
// commit.
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the vulnerability.
|
// The only notes we take are if we encountered unknown Ubuntu release.
|
||||||
v, unknownReleases, err := parseUbuntuCVE(file)
|
// We don't want the commit to be considered as managed in that case.
|
||||||
if err != nil {
|
if len(resp.Notes) != 0 {
|
||||||
return resp, err
|
resp.FlagValue = dbCommit
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *updater) Clean() {
|
func (u *updater) Clean() {
|
||||||
|
if u.repositoryLocalPath != "" {
|
||||||
os.RemoveAll(u.repositoryLocalPath)
|
os.RemoveAll(u.repositoryLocalPath)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (u *updater) pullRepository() (err error) {
|
func (u *updater) pullRepository() (commit string, err error) {
|
||||||
// Determine whether we should branch or pull.
|
// Determine whether we should branch or pull.
|
||||||
if _, pathExists := os.Stat(u.repositoryLocalPath); u.repositoryLocalPath == "" || os.IsNotExist(pathExists) {
|
if _, pathExists := os.Stat(u.repositoryLocalPath); u.repositoryLocalPath == "" || os.IsNotExist(pathExists) {
|
||||||
// Create a temporary folder to store the repository.
|
// Create a temporary folder to store the repository.
|
||||||
if u.repositoryLocalPath, err = ioutil.TempDir(os.TempDir(), "ubuntu-cve-tracker"); err != nil {
|
if u.repositoryLocalPath, err = ioutil.TempDir(os.TempDir(), "ubuntu-cve-tracker"); err != nil {
|
||||||
return vulnsrc.ErrFilesystem
|
return "", vulnsrc.ErrFilesystem
|
||||||
}
|
}
|
||||||
|
cmd := exec.Command("git", "clone", trackerURI, ".")
|
||||||
// Branch repository.
|
|
||||||
cmd := exec.Command("bzr", "branch", "--use-existing-dir", trackerRepository, ".")
|
|
||||||
cmd.Dir = u.repositoryLocalPath
|
cmd.Dir = u.repositoryLocalPath
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
log.WithError(err).WithField("output", string(out)).Error("could not branch Ubuntu repository")
|
u.Clean()
|
||||||
return commonerr.ErrCouldNotDownload
|
log.WithError(err).WithField("output", string(out)).Error("could not clone ubuntu-cve-tracker repository")
|
||||||
|
return "", commonerr.ErrCouldNotDownload
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return nil
|
// The repository already exists and it needs to be refreshed via a pull.
|
||||||
}
|
cmd := exec.Command("git", "pull")
|
||||||
|
|
||||||
// Pull repository.
|
|
||||||
cmd := exec.Command("bzr", "pull", "--overwrite")
|
|
||||||
cmd.Dir = u.repositoryLocalPath
|
cmd.Dir = u.repositoryLocalPath
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
if _, err := cmd.CombinedOutput(); err != nil {
|
||||||
os.RemoveAll(u.repositoryLocalPath)
|
return "", vulnsrc.ErrGitFailure
|
||||||
log.WithError(err).WithField("output", string(out)).Error("could not pull Ubuntu repository")
|
}
|
||||||
return commonerr.ErrCouldNotDownload
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
cmd := exec.Command("git", "rev-parse", "HEAD")
|
||||||
}
|
cmd.Dir = u.repositoryLocalPath
|
||||||
|
|
||||||
func getRevisionNumber(pathToRepo string) (int, error) {
|
|
||||||
cmd := exec.Command("bzr", "revno")
|
|
||||||
cmd.Dir = pathToRepo
|
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).WithField("output", string(out)).Error("could not get Ubuntu repository's revision number")
|
return "", vulnsrc.ErrGitFailure
|
||||||
return 0, commonerr.ErrCouldNotDownload
|
|
||||||
}
|
}
|
||||||
|
|
||||||
revno, err := strconv.Atoi(strings.TrimSpace(string(out)))
|
commit = strings.TrimSpace(string(out))
|
||||||
if err != nil {
|
return
|
||||||
log.WithError(err).WithField("output", string(out)).Error("could not parse Ubuntu repository's revision number")
|
|
||||||
return 0, commonerr.ErrCouldNotDownload
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return revno, nil
|
func collectModifiedVulnerabilities(commit, dbCommit, repositoryLocalPath string) (map[string]struct{}, error) {
|
||||||
}
|
|
||||||
|
|
||||||
func collectModifiedVulnerabilities(revision int, dbRevision, repositoryLocalPath string) (map[string]struct{}, error) {
|
|
||||||
modifiedCVE := make(map[string]struct{})
|
modifiedCVE := make(map[string]struct{})
|
||||||
|
for _, dirName := range []string{"active", "retired"} {
|
||||||
|
if err := processDirectory(repositoryLocalPath, dirName, modifiedCVE); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modifiedCVE, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Handle a brand new database.
|
func processDirectory(repositoryLocalPath, dirName string, modifiedCVE map[string]struct{}) error {
|
||||||
if dbRevision == "" {
|
// Open the directory.
|
||||||
for _, folder := range []string{"active", "retired"} {
|
d, err := os.Open(repositoryLocalPath + "/" + dirName)
|
||||||
d, err := os.Open(repositoryLocalPath + "/" + folder)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("could not open Ubuntu vulnerabilities repository's folder")
|
log.WithError(err).Error("could not open Ubuntu vulnerabilities repository's folder")
|
||||||
return nil, vulnsrc.ErrFilesystem
|
return vulnsrc.ErrFilesystem
|
||||||
}
|
}
|
||||||
|
defer d.Close()
|
||||||
|
|
||||||
// Get the FileInfo of all the files in the directory.
|
// Get the FileInfo of all the files in the directory.
|
||||||
names, err := d.Readdirnames(-1)
|
names, err := d.Readdirnames(-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("could not read Ubuntu vulnerabilities repository's folder")
|
log.WithError(err).Error("could not read Ubuntu vulnerabilities repository's folder")
|
||||||
return nil, vulnsrc.ErrFilesystem
|
return vulnsrc.ErrFilesystem
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the vulnerabilities to the list.
|
// Add the vulnerabilities to the list.
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
if strings.HasPrefix(name, "CVE-") {
|
if strings.HasPrefix(name, "CVE-") {
|
||||||
modifiedCVE[folder+"/"+name] = struct{}{}
|
modifiedCVE[dirName+"/"+name] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the file manually.
|
return nil
|
||||||
//
|
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return modifiedCVE, nil
|
func collectVulnerabilitiesAndNotes(repositoryLocalPath string, modifiedCVE map[string]struct{}) ([]database.VulnerabilityWithAffected, []string, error) {
|
||||||
}
|
vulns := make([]database.VulnerabilityWithAffected, 0)
|
||||||
|
noteSet := make(map[string]struct{})
|
||||||
|
|
||||||
// Handle an up to date database.
|
for cvePath := range modifiedCVE {
|
||||||
dbRevisionInt, _ := strconv.Atoi(dbRevision)
|
// Open the CVE file.
|
||||||
if revision == dbRevisionInt {
|
file, err := os.Open(repositoryLocalPath + "/" + cvePath)
|
||||||
log.WithField("package", "Ubuntu").Debug("no update")
|
|
||||||
return modifiedCVE, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle a database that needs upgrading.
|
|
||||||
cmd := exec.Command("bzr", "log", "--verbose", "-r"+strconv.Itoa(dbRevisionInt+1)+"..", "-n0")
|
|
||||||
cmd.Dir = repositoryLocalPath
|
|
||||||
out, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).WithField("output", string(out)).Error("could not get Ubuntu vulnerabilities repository logs")
|
// This can happen when a file is modified then moved in another commit.
|
||||||
return nil, commonerr.ErrCouldNotDownload
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(out))
|
// Parse the vulnerability.
|
||||||
for scanner.Scan() {
|
v, unknownReleases, err := parseUbuntuCVE(file)
|
||||||
text := strings.TrimSpace(scanner.Text())
|
if err != nil {
|
||||||
if strings.Contains(text, "CVE-") && (strings.HasPrefix(text, "active/") || strings.HasPrefix(text, "retired/")) {
|
file.Close()
|
||||||
if strings.Contains(text, " => ") {
|
return nil, nil, err
|
||||||
text = text[strings.Index(text, " => ")+4:]
|
|
||||||
}
|
|
||||||
modifiedCVE[text] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return modifiedCVE, nil
|
// Add the vulnerability to the response.
|
||||||
|
vulns = append(vulns, v)
|
||||||
|
|
||||||
|
// Store any unknown releases as notes.
|
||||||
|
for k := range unknownReleases {
|
||||||
|
noteSet[errUnknownRelease.Error()+": "+k] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the note set into a slice.
|
||||||
|
var notes []string
|
||||||
|
for note := range noteSet {
|
||||||
|
notes = append(notes, note)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vulns, notes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseUbuntuCVE(fileContent io.Reader) (vulnerability database.VulnerabilityWithAffected, unknownReleases map[string]struct{}, err error) {
|
func parseUbuntuCVE(fileContent io.Reader) (vulnerability database.VulnerabilityWithAffected, unknownReleases map[string]struct{}, err error) {
|
||||||
|
Loading…
Reference in New Issue
Block a user