Skip to content

Commit

Permalink
Store vulnerability report from scanner into a relational format
Browse files Browse the repository at this point in the history
Convert vulnerability report JSON obtained  from scanner into a relational format describe in:goharbor/community#145

Signed-off-by: prahaladdarkin <prahaladd@vmware.com>
  • Loading branch information
prahaladdarkin committed Dec 2, 2020
1 parent be4e6a5 commit 145cb5a
Show file tree
Hide file tree
Showing 10 changed files with 951 additions and 138 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
!/contrib/helm/harbor

!/contrib/helm/harbor
*.code-workspace
make/harbor.yml
make/docker-compose.yml
make/common/config/*
Expand Down Expand Up @@ -47,3 +48,5 @@ src/core/conf/app.conf

src/server/v2.0/models/
src/server/v2.0/restapi/
pkg/
pkh/**
41 changes: 41 additions & 0 deletions make/migrations/postgresql/0050_2.2.0_schema.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,44 @@ BEGIN
END $$;

DROP TABLE IF EXISTS replication_schedule_job;

/*
Common vulnerability reporting schema.
Github proposal link : https://github.com/goharbor/community/pull/145
*/

-- --------------------------------------------------
-- Table Structure for `main.VulnerabilityRecord`
-- --------------------------------------------------
CREATE TABLE IF NOT EXISTS "vulnerability_record_v2" (
"id" serial NOT NULL PRIMARY KEY,
"cve_id" text NOT NULL DEFAULT '' ,
"registration_uuid" text NOT NULL DEFAULT '',
"digest" text NOT NULL DEFAULT '' ,
"report_uuid" text NOT NULL DEFAULT '' ,
"package" text NOT NULL DEFAULT '' ,
"package_version" text NOT NULL DEFAULT '' ,
"package_type" text NOT NULL DEFAULT '' ,
"severity" text NOT NULL DEFAULT '' ,
"fixed_version" text,
"urls" text,
"cve3_score" double precision,
"cve2_score" double precision,
"cvss3_vector" text,
"cvss2_vector" text,
"description" text,
"cwe_ids" text,
"vendorattributes" json,
UNIQUE ("cve_id", "registration_uuid", "package", "package_version", "digest")
);

-- --------------------------------------------------
-- Table Structure for `main.ReportVulnerabilityRecord`
-- --------------------------------------------------
CREATE TABLE IF NOT EXISTS "report_vulnerability_record_v2" (
"id" serial NOT NULL PRIMARY KEY,
"report_uuid" text NOT NULL DEFAULT '' ,
"vuln_record_id" bigint NOT NULL DEFAULT 0 ,
UNIQUE ("report_uuid", "vuln_record_id")
);

16 changes: 16 additions & 0 deletions src/controller/scan/base_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/goharbor/harbor/src/pkg/scan/all"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/postprocessors"
"github.com/goharbor/harbor/src/pkg/scan/report"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
Expand Down Expand Up @@ -495,6 +496,7 @@ func (bc *basicController) GetScanLog(uuid string) ([]byte, error) {

// HandleJobHooks ...
func (bc *basicController) HandleJobHooks(trackID string, change *job.StatusChange) error {
log.Infof("Executing HandleJobHook for track ID : %s", trackID)
if len(trackID) == 0 {
return errors.New("empty track ID")
}
Expand Down Expand Up @@ -552,6 +554,20 @@ func (bc *basicController) HandleJobHooks(trackID string, change *job.StatusChan
return errors.Wrap(err, "scan controller: handle job hook")
}

//at this point the scan is complete and the JSON raw report data is available.
//convert it to the v2 report format and persist into the database.
//get the complete report definition and then convert to the new schema
report, err := bc.manager.Get(rpl[0].UUID)
if err != nil {
return errors.Wrapf(err, "scan controller: handle job hook report conversion failure for report %s", rpl[0].UUID)
}
log.Infof("Converting report ID %s to the new V2 schema", rpl[0].UUID)
rc := postprocessors.NewScanReportV1ToV2Converter()
_, err = rc.Convert(report)
if err != nil {
return errors.Wrapf(err, "Failed to convert vulnerability data to new schema for report UUID : %s", rpl[0].UUID)
}
log.Infof("Converted report ID %s to the new V2 schema", rpl[0].UUID)
return nil
}

Expand Down
140 changes: 3 additions & 137 deletions src/go.sum

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions src/pkg/scan/dao/scanv2/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright Project Harbor 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 scanv2

// VulnerabilityRecord of an individual vulnerability. Identifies an individual vulnerability item in the scan.
// Since multiple scanners could be registered with the projects, each scanner
// would have it's own definition for the same CVE ID. Hence a CVE ID is qualified along
// with the ID of the scanner that owns the CVE record definition.
// The scanner ID would be the same as the RegistrationUUID field of Report.
// Identified by the `cve_id` and `registration_uuid`.
// Relates to the image using the `digest` and to the report using the `report UUID` field
type VulnerabilityRecord struct {
ID int64 `orm:"pk;auto;column(id)"`
CVEID string `orm:"column(cve_id)"`
RegistrationUUID string `orm:"column(registration_uuid)"`
Digest string `orm:"column(digest)"`
Report string `orm:"column(report_uuid)"`
Package string `orm:"column(package)"`
PackageVersion string `orm:"column(package_version)"`
PackageType string `orm:"column(package_type)"`
Severity string `orm:"column(severity)"`
Fix string `orm:"column(fixed_version);null"`
URL string `orm:"column(urls);null"`
CVE3Score float64 `orm:"column(cve3_score);null"`
CVE2Score float64 `orm:"column(cve2_score);null"`
CVSS3Vector string `orm:"column(cvss3_vector);null"` //e.g. CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
CVSS2Vector string `orm:"column(cvss2_vector);null"` //e.g. AV:L/AC:M/Au:N/C:P/I:N/A:N
Description string `orm:"column(description);null"`
CWEIds string `orm:"column(cwe_ids);null"` //e.g. CWE-476,CWE-123,CWE-234
VendorAttributes string `orm:"column(vendorattributes);type(json);null"`
}

//ReportVulnerabilityRecord is relation table required to optimize data storage for both the
//vulnerability records and the scan report.
//identified by composite key (ID, Report)
//Since each scan report has a separate UUID, the composite key
//would ensure that the immutability of the historical scan reports is guaranteed.
//It is sufficient to store the int64 VulnerabilityRecord Id since the vulnerability records
//are uniquely identified in the table based on the ScannerID and the CVEID
type ReportVulnerabilityRecord struct {
ID int64 `orm:"pk;auto;column(id)"`
Report string `orm:"column(report_uuid);"`
VulnRecordID int64 `orm:"column(vuln_record_id);"`
}

//TableName for VulnerabilityRecord
func (vr *VulnerabilityRecord) TableName() string {
return "vulnerability_record_v2"
}

//TableUnique for VulnerabilityRecord
func (vr *VulnerabilityRecord) TableUnique() [][]string {
return [][]string{
{"cve_id", "registration_uuid", "package", "package_version", "digest"},
}
}

//TableName for ReportVulnerabilityRecord
func (rvr *ReportVulnerabilityRecord) TableName() string {
return "report_vulnerability_record_v2"
}

//TableUnique for ReportVulnerabilityRecord
func (rvr *ReportVulnerabilityRecord) TableUnique() [][]string {
return [][]string{
{"report_uuid", "vuln_record_id"},
}
}
150 changes: 150 additions & 0 deletions src/pkg/scan/dao/scanv2/report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright Project Harbor 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 scanv2

import (
"fmt"

"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/lib/q"
)

func init() {
orm.RegisterModel(new(VulnerabilityRecord), new(ReportVulnerabilityRecord))
}

// CreateVulnerabilityRecord creates new vulnerability record.
func CreateVulnerabilityRecord(vr *VulnerabilityRecord) (int64, error) {
o := dao.GetOrmer()
_, vrID, err := o.ReadOrCreate(vr, "CVEID", "RegistrationUUID", "Digest", "Package", "PackageVersion")

return vrID, err
}

//DeleteVulnerabilityRecord deletes a vulnerability record
func DeleteVulnerabilityRecord(vr *VulnerabilityRecord) error {
o := dao.GetOrmer()
_, err := o.Delete(vr, "CVEID", "RegistrationUUID")

return err
}

// ListVulnerabilityRecords lists the vulnerability records with given query parameters.
// Keywords in query here will be enforced with `exact` way.
// If the registration ID (which = the scanner ID is not specified), the results
// would contain duplicate records for a CVE depending upon the number of registered
// scanners which individually store data about the CVE. In such cases, it is the
// responsibility of the calling code to de-duplicate the CVE records or bucket them
// per registered scanner
func ListVulnerabilityRecords(query *q.Query) ([]*VulnerabilityRecord, error) {
o := dao.GetOrmer()
qt := o.QueryTable(new(VulnerabilityRecord))

if query != nil {
if len(query.Keywords) > 0 {
for k, v := range query.Keywords {
if vv, ok := v.([]interface{}); ok {
qt = qt.Filter(fmt.Sprintf("%s__in", k), vv...)
continue
}

qt = qt.Filter(k, v)
}
}

if query.PageNumber > 0 && query.PageSize > 0 {
qt = qt.Limit(query.PageSize, (query.PageNumber-1)*query.PageSize)
}
}

l := make([]*VulnerabilityRecord, 0)
_, err := qt.All(&l)

return l, err
}

//InsertVulnerabilityDataForReport inserts a vulnerability record in the context of scan report
func InsertVulnerabilityDataForReport(reportUUID string, vr *VulnerabilityRecord) (int64, error) {

vrID, err := CreateVulnerabilityRecord(vr)

if err != nil {
return vrID, err
}

rvr := new(ReportVulnerabilityRecord)
rvr.Report = reportUUID
rvr.VulnRecordID = vrID

o := dao.GetOrmer()
_, rvrID, err := o.ReadOrCreate(rvr, "report_uuid", "vuln_record_id")

return rvrID, err

}

//DeleteAllVulnerabilityRecordsForReport deletes the vulnerability records for a single report
func DeleteAllVulnerabilityRecordsForReport(reportUUID string) (int64, error) {
o := dao.GetOrmer()
delCount, err := o.Delete(&ReportVulnerabilityRecord{Report: reportUUID}, "report_uuid")
return delCount, err
}

// GetAllVulnerabilityRecordsForReport gets all the vulnerability records for a report
func GetAllVulnerabilityRecordsForReport(reportUUID string) ([]*VulnerabilityRecord, error) {
vulnRecs := make([]*VulnerabilityRecord, 0)
o := dao.GetOrmer()
query := `select vulnerability_record_v2.* from vulnerability_record_v2
inner join report_vulnerability_record_v2 on
vulnerability_record_v2.id = report_vulnerability_record_v2.vuln_record_id and report_vulnerability_record_v2.report_uuid=?`
_, err := o.Raw(query, reportUUID).QueryRows(&vulnRecs)
return vulnRecs, err
}

// GetVulnerabilityRecordsForScanner gets all the vulnerability records known to a scanner
// identified by registrationUUID
func GetVulnerabilityRecordsForScanner(registrationUUID string) ([]*VulnerabilityRecord, error) {
var vulnRecs []*VulnerabilityRecord
o := dao.GetOrmer()
vulRec := new(VulnerabilityRecord)
qs := o.QueryTable(vulRec)
_, err := qs.Filter("registration_uuid", registrationUUID).All(&vulnRecs)
if err != nil {
return nil, err
}
return vulnRecs, nil
}

// DeleteVulnerabilityRecordsForScanner deletes all the vulnerability records for a given scanner
// identified by registrationUUID
func DeleteVulnerabilityRecordsForScanner(registrationUUID string) (int64, error) {
o := dao.GetOrmer()
vulnRec := new(VulnerabilityRecord)
vulnRec.RegistrationUUID = registrationUUID
return o.Delete(vulnRec, "registration_uuid")
}

// GetVulnerabilityRecordIdsForScanner retrieves the internal Ids of the vulnerability records for a given scanner
// identified by registrationUUID
func GetVulnerabilityRecordIdsForScanner(registrationUUID string) ([]int, error) {
vulnRecordIds := make([]int, 0)
o := dao.GetOrmer()
_, err := o.Raw("select id from vulnerability_record_v2 where registration_uuid = ?", registrationUUID).QueryRows(&vulnRecordIds)
if err != nil {
return vulnRecordIds, err
}
return vulnRecordIds, err
}

0 comments on commit 145cb5a

Please sign in to comment.