1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-14 11:03:09 +02:00
sap-jenkins-library/pkg/checkmarx/checkmarx.go

704 lines
22 KiB
Go
Raw Normal View History

package checkmarx
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"encoding/xml"
piperHttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// AuthToken - Structure to store OAuth2 token
type AuthToken struct {
TokenType string `json:"token_type"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
// Preset - Project's Preset
type Preset struct {
ID int `json:"id"`
Name string `json:"name"`
OwnerName string `json:"ownerName"`
Link Link `json:"link"`
}
// Scan - Scan Structure
type Scan struct {
ID int `json:"id"`
Link Link `json:"link"`
}
// ProjectCreateResult - ProjectCreateResult Structure
type ProjectCreateResult struct {
ID int `json:"id"`
Link Link `json:"link"`
}
// Report - Report Structure
type Report struct {
ReportID int `json:"reportId"`
Links Links `json:"links"`
}
// ResultsStatistics - ResultsStatistics Structure
type ResultsStatistics struct {
High int `json:"highSeverity"`
Medium int `json:"mediumSeverity"`
Low int `json:"lowSeverity"`
Info int `json:"infoSeverity"`
}
// ScanStatus - ScanStatus Structure
type ScanStatus struct {
ID int `json:"id"`
Link Link `json:"link"`
Status Status `json:"status"`
ScanType string `json:"scanType"`
Comment string `json:"comment"`
IsIncremental bool `json:"isIncremental"`
}
// Status - Status Structure
type Status struct {
ID int `json:"id"`
Name string `json:"name"`
Details ScanStatusDetail `json:"details"`
}
// ScanStatusDetail - ScanStatusDetail Structure
type ScanStatusDetail struct {
Stage string `json:"stage"`
Step string `json:"step"`
}
// ReportStatusResponse - ReportStatusResponse Structure
type ReportStatusResponse struct {
Location string `json:"location"`
ContentType string `json:"contentType"`
Status ReportStatus `json:"status"`
}
// ReportStatus - ReportStatus Structure
type ReportStatus struct {
ID int `json:"id"`
Value string `json:"value"`
}
// Project - Project Structure
type Project struct {
ID int `json:"id"`
TeamID string `json:"teamId"`
Name string `json:"name"`
IsPublic bool `json:"isPublic"`
SourceSettingsLink SourceSettingsLink `json:"sourceSettingsLink"`
Link Link `json:"link"`
}
// Team - Team Structure
type Team struct {
ID string `json:"id"`
FullName string `json:"fullName"`
}
// Links - Links Structure
type Links struct {
Report Link `json:"report"`
Status Link `json:"status"`
}
// Link - Link Structure
type Link struct {
Rel string `json:"rel"`
URI string `json:"uri"`
}
// SourceSettingsLink - SourceSettingsLink Structure
type SourceSettingsLink struct {
Type string `json:"type"`
Rel string `json:"rel"`
URI string `json:"uri"`
}
//DetailedResult - DetailedResult Structure
type DetailedResult struct {
XMLName xml.Name `xml:"CxXMLResults"`
InitiatorName string `xml:"InitiatorName,attr"`
ScanID string `xml:"ScanId,attr"`
Owner string `xml:"Owner,attr"`
ProjectID string `xml:"ProjectId,attr"`
ProjectName string `xml:"ProjectName,attr"`
TeamFullPathOnReportDate string `xml:"TeamFullPathOnReportDate,attr"`
DeepLink string `xml:"DeepLink,attr"`
ScanStart string `xml:"ScanStart,attr"`
Preset string `xml:"Preset,attr"`
ScanTime string `xml:"ScanTime,attr"`
LinesOfCodeScanned string `xml:"LinesOfCodeScanned,attr"`
FilesScanned string `xml:"FilesScanned,attr"`
ReportCreationTime string `xml:"ReportCreationTime,attr"`
Team string `xml:"Team,attr"`
CheckmarxVersion string `xml:"CheckmarxVersion,attr"`
ScanType string `xml:"ScanType,attr"`
SourceOrigin string `xml:"SourceOrigin,attr"`
Visibility string `xml:"Visibility,attr"`
Queries []Query `xml:"Query"`
}
// Query - Query Structure
type Query struct {
XMLName xml.Name `xml:"Query"`
Results []Result `xml:"Result"`
}
// Result - Result Structure
type Result struct {
XMLName xml.Name `xml:"Result"`
State string `xml:"state,attr"`
Severity string `xml:"Severity,attr"`
FalsePositive string `xml:"FalsePositive,attr"`
}
// SystemInstance is the client communicating with the Checkmarx backend
type SystemInstance struct {
serverURL string
username string
password string
client piperHttp.Uploader
logger *logrus.Entry
}
// System is the interface abstraction of a specific SystemIns
type System interface {
FilterPresetByName(presets []Preset, presetName string) Preset
FilterPresetByID(presets []Preset, presetID int) Preset
FilterProjectByName(projects []Project, projectName string) Project
FilterTeamByName(teams []Team, teamName string) Team
FilterTeamByID(teams []Team, teamID string) Team
DownloadReport(reportID int) (bool, []byte)
GetReportStatus(reportID int) ReportStatusResponse
RequestNewReport(scanID int, reportType string) (bool, Report)
GetResults(scanID int) ResultsStatistics
GetScanStatusAndDetail(scanID int) (string, ScanStatusDetail)
GetScans(projectID int) (bool, []ScanStatus)
ScanProject(projectID int, isIncremental, isPublic, forceScan bool) (bool, Scan)
UpdateProjectConfiguration(projectID int, presetID int, engineConfigurationID string) bool
UpdateProjectExcludeSettings(projectID int, excludeFolders string, excludeFiles string) bool
UploadProjectSourceCode(projectID int, zipFile string) bool
CreateProject(projectName string, teamID string) (bool, ProjectCreateResult)
CreateBranch(projectID int, branchName string) int
GetPresets() []Preset
GetProjectByID(projectID int) (bool, Project)
GetProjectsByNameAndTeam(projectName, teamID string) []Project
GetProjects() []Project
GetTeams() []Team
}
// NewSystemInstance returns a new Checkmarx client for communicating with the backend
func NewSystemInstance(client piperHttp.Uploader, serverURL, username, password string) (*SystemInstance, error) {
loggerInstance := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx")
sys := &SystemInstance{
serverURL: serverURL,
username: username,
password: password,
client: client,
logger: loggerInstance,
}
token, err := sys.getOAuth2Token()
if err != nil {
return sys, errors.Wrap(err, "Error fetching oAuth token")
}
options := piperHttp.ClientOptions{
Token: token,
Timeout: time.Second * 60,
}
sys.client.SetOptions(options)
return sys, nil
}
func sendRequest(sys *SystemInstance, method, url string, body io.Reader, header http.Header) ([]byte, error) {
return sendRequestInternal(sys, method, url, body, header, "200:399")
}
func sendRequestInternal(sys *SystemInstance, method, url string, body io.Reader, header http.Header, validStatusCodeRange string) ([]byte, error) {
var requestBody io.Reader
var requestBodyCopy io.Reader
if body != nil {
closer := ioutil.NopCloser(body)
bodyBytes, _ := ioutil.ReadAll(closer)
requestBody = bytes.NewBuffer(bodyBytes)
requestBodyCopy = bytes.NewBuffer(bodyBytes)
defer closer.Close()
}
response, err := sys.client.SendRequest(method, fmt.Sprintf("%v/cxrestapi%v", sys.serverURL, url), requestBody, header, nil)
if err != nil {
sys.recordRequestDetailsInErrorCase(requestBodyCopy, response)
sys.logger.Errorf("HTTP request failed with error: %s", err)
return nil, err
}
var validResponseCodeList []int
values := strings.Split(validStatusCodeRange, ",")
for _, value := range values {
parts := strings.Split(value, ":")
if len(parts) > 1 {
lower, _ := strconv.Atoi(parts[0])
upper, _ := strconv.Atoi(parts[1])
for i := lower; i <= upper; i++ {
validResponseCodeList = append(validResponseCodeList, i)
}
} else {
validCode, _ := strconv.Atoi(value)
validResponseCodeList = append(validResponseCodeList, validCode)
}
}
if piperutils.ContainsInt(validResponseCodeList, response.StatusCode) {
data, _ := ioutil.ReadAll(response.Body)
sys.logger.Debugf("Valid response body: %v", string(data))
defer response.Body.Close()
return data, nil
}
sys.recordRequestDetailsInErrorCase(requestBodyCopy, response)
sys.logger.Errorf("HTTP request failed with error %s", response.Status)
return nil, errors.Errorf("Invalid HTTP status %v with with code %v received", response.Status, response.StatusCode)
}
func (sys *SystemInstance) recordRequestDetailsInErrorCase(requestBody io.Reader, response *http.Response) {
if requestBody != nil {
data, _ := ioutil.ReadAll(ioutil.NopCloser(requestBody))
sys.logger.Errorf("Request body: %s", data)
}
if response != nil && response.Body != nil {
data, _ := ioutil.ReadAll(response.Body)
sys.logger.Errorf("Response body: %s", data)
response.Body.Close()
}
}
func (sys *SystemInstance) getOAuth2Token() (string, error) {
body := url.Values{
"username": {sys.username},
"password": {sys.password},
"grant_type": {"password"},
"scope": {"sast_rest_api"},
"client_id": {"resource_owner_client"},
"client_secret": {"014DF517-39D1-4453-B7B3-9930C563627C"},
}
header := http.Header{}
header.Add("Content-type", "application/x-www-form-urlencoded")
data, err := sendRequest(sys, http.MethodPost, "/auth/identity/connect/token", strings.NewReader(body.Encode()), header)
if err != nil {
return "", err
}
var token AuthToken
json.Unmarshal(data, &token)
return token.TokenType + " " + token.AccessToken, nil
}
// GetTeams returns the teams the user is assigned to
func (sys *SystemInstance) GetTeams() []Team {
sys.logger.Debug("Getting Teams...")
var teams []Team
data, err := sendRequest(sys, http.MethodGet, "/auth/teams", nil, nil)
if err != nil {
sys.logger.Errorf("Fetching teams failed: %s", err)
return teams
}
json.Unmarshal(data, &teams)
return teams
}
// GetProjects returns the projects defined in the Checkmarx backend which the user has access to
func (sys *SystemInstance) GetProjects() []Project {
return sys.GetProjectsByNameAndTeam("", "")
}
// GetProjectByID returns the project addressed by projectID from the Checkmarx backend which the user has access to
func (sys *SystemInstance) GetProjectByID(projectID int) (bool, Project) {
sys.logger.Debugf("Getting Project with ID %v...", projectID)
var project Project
data, err := sendRequest(sys, http.MethodGet, fmt.Sprintf("/projects/%v", projectID), nil, nil)
if err != nil {
sys.logger.Errorf("Fetching projects failed: %s", err)
return false, project
}
json.Unmarshal(data, &project)
return true, project
}
// GetProjectsByNameAndTeam returns the project addressed by projectID from the Checkmarx backend which the user has access to
func (sys *SystemInstance) GetProjectsByNameAndTeam(projectName, teamID string) []Project {
sys.logger.Debugf("Getting projects with name %v of team %v...", projectName, teamID)
var projects []Project
header := http.Header{}
header.Set("Accept-Type", "application/json")
var data []byte
var err error
if len(teamID) > 0 && len(projectName) > 0 {
body := url.Values{
"projectName": {projectName},
"teamId": {teamID},
}
data, err = sendRequestInternal(sys, http.MethodGet, fmt.Sprintf("/projects?%v", body.Encode()), nil, header, "200:399,404")
} else {
data, err = sendRequestInternal(sys, http.MethodGet, "/projects", nil, header, "200:399,404")
}
if err != nil {
sys.logger.Errorf("Fetching projects failed: %s", err)
return projects
}
json.Unmarshal(data, &projects)
return projects
}
// CreateProject creates a new project in the Checkmarx backend
func (sys *SystemInstance) CreateProject(projectName string, teamID string) (bool, ProjectCreateResult) {
var result ProjectCreateResult
jsonData := map[string]interface{}{
"name": projectName,
"owningTeam": teamID,
"isPublic": true,
}
jsonValue, err := json.Marshal(jsonData)
if err != nil {
sys.logger.Errorf("Error Marshal: %s", err)
return false, result
}
header := http.Header{}
header.Set("Content-Type", "application/json")
data, err := sendRequest(sys, http.MethodPost, "/projects", bytes.NewBuffer(jsonValue), header)
if err != nil {
sys.logger.Errorf("Failed to create project: %s", err)
return false, result
}
json.Unmarshal(data, &result)
return true, result
}
// CreateBranch creates a branch of an existing project in the Checkmarx backend
func (sys *SystemInstance) CreateBranch(projectID int, branchName string) int {
jsonData := map[string]interface{}{
"name": branchName,
}
jsonValue, err := json.Marshal(jsonData)
if err != nil {
sys.logger.Errorf("Error Marshal: %s", err)
return 0
}
header := http.Header{}
header.Set("Content-Type", "application/json")
data, err := sendRequest(sys, http.MethodPost, fmt.Sprintf("/projects/%v/branch", projectID), bytes.NewBuffer(jsonValue), header)
if err != nil {
sys.logger.Errorf("Failed to create project: %s", err)
return 0
}
var scan Scan
json.Unmarshal(data, &scan)
return scan.ID
}
// UploadProjectSourceCode zips and uploads the project sources for scanning
func (sys *SystemInstance) UploadProjectSourceCode(projectID int, zipFile string) bool {
sys.logger.Debug("Starting to upload files...")
header := http.Header{}
header.Add("Accept-Encoding", "gzip,deflate")
header.Add("Accept", "text/plain")
resp, err := sys.client.UploadFile(fmt.Sprintf("%v/cxrestapi/projects/%v/sourceCode/attachments", sys.serverURL, projectID), zipFile, "zippedSource", header, nil)
if err != nil {
sys.logger.Errorf("Failed to uploaded zipped sources %s", err)
return false
}
data, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
sys.logger.Errorf("Error reading the response data %s", err)
return false
}
responseData := make(map[string]string)
json.Unmarshal(data, &responseData)
if resp.StatusCode == http.StatusNoContent {
return true
}
sys.logger.Debugf("Body %s", data)
sys.logger.Errorf("Error writing the request's body: %s", resp.Status)
return false
}
// UpdateProjectExcludeSettings updates the exclude configuration of the project
func (sys *SystemInstance) UpdateProjectExcludeSettings(projectID int, excludeFolders string, excludeFiles string) bool {
jsonData := map[string]string{
"excludeFoldersPattern": excludeFolders,
"excludeFilesPattern": excludeFiles,
}
jsonValue, err := json.Marshal(jsonData)
if err != nil {
sys.logger.Errorf("Error Marshal: %s", err)
return false
}
header := http.Header{}
header.Set("Content-Type", "application/json")
_, err = sendRequest(sys, http.MethodPut, fmt.Sprintf("/projects/%v/sourceCode/excludeSettings", projectID), bytes.NewBuffer(jsonValue), header)
if err != nil {
sys.logger.Errorf("HTTP request failed with error: %s", err)
return false
}
return true
}
// GetPresets loads the preset values defined in the Checkmarx backend
func (sys *SystemInstance) GetPresets() []Preset {
sys.logger.Debug("Getting Presets...")
var presets []Preset
data, err := sendRequest(sys, http.MethodGet, "/sast/presets", nil, nil)
if err != nil {
sys.logger.Errorf("Fetching presets failed: %s", err)
return presets
}
json.Unmarshal(data, &presets)
return presets
}
// UpdateProjectConfiguration updates the configuration of the project addressed by projectID
func (sys *SystemInstance) UpdateProjectConfiguration(projectID int, presetID int, engineConfigurationID string) bool {
engineConfigID, _ := strconv.Atoi(engineConfigurationID)
jsonData := map[string]interface{}{
"projectId": projectID,
"presetId": presetID,
"engineConfigurationId": engineConfigID,
}
jsonValue, err := json.Marshal(jsonData)
if err != nil {
sys.logger.Errorf("Error marshal: %s", err)
return false
}
header := http.Header{}
header.Set("Content-Type", "application/json")
_, err = sendRequest(sys, http.MethodPost, "/sast/scanSettings", bytes.NewBuffer(jsonValue), header)
if err != nil {
sys.logger.Errorf("HTTP request failed with error: %s", err)
return false
}
return true
}
// ScanProject triggers a scan on the project addressed by projectID
func (sys *SystemInstance) ScanProject(projectID int, isIncremental, isPublic, forceScan bool) (bool, Scan) {
scan := Scan{}
jsonData := map[string]interface{}{
"projectId": projectID,
"isIncremental": false,
"isPublic": true,
"forceScan": true,
"comment": "Scan From Golang Script",
}
jsonValue, _ := json.Marshal(jsonData)
header := http.Header{}
header.Set("cxOrigin", "GolangScript")
header.Set("Content-Type", "application/json")
data, err := sendRequest(sys, http.MethodPost, "/sast/scans", bytes.NewBuffer(jsonValue), header)
if err != nil {
sys.logger.Errorf("Failed to trigger scan of project %v: %s", projectID, err)
return false, scan
}
json.Unmarshal(data, &scan)
return true, scan
}
// GetScans returns all scan status on the project addressed by projectID
func (sys *SystemInstance) GetScans(projectID int) (bool, []ScanStatus) {
scans := []ScanStatus{}
body := url.Values{
"projectId": {fmt.Sprintf("%v", projectID)},
"last": {fmt.Sprintf("%v", 20)},
}
header := http.Header{}
header.Set("cxOrigin", "GolangScript")
header.Set("Accept-Type", "application/json")
data, err := sendRequest(sys, http.MethodGet, fmt.Sprintf("/sast/scans?%v", body.Encode()), nil, header)
if err != nil {
sys.logger.Errorf("Failed to fetch scans of project %v: %s", projectID, err)
return false, scans
}
json.Unmarshal(data, &scans)
return true, scans
}
// GetScanStatusAndDetail returns the status of the scan addressed by scanID
func (sys *SystemInstance) GetScanStatusAndDetail(scanID int) (string, ScanStatusDetail) {
var scanStatus ScanStatus
data, err := sendRequest(sys, http.MethodGet, fmt.Sprintf("/sast/scans/%v", scanID), nil, nil)
if err != nil {
sys.logger.Errorf("Failed to get scan status for scanID %v: %s", scanID, err)
return "Failed", ScanStatusDetail{}
}
json.Unmarshal(data, &scanStatus)
return scanStatus.Status.Name, scanStatus.Status.Details
}
// GetResults returns the results of the scan addressed by scanID
func (sys *SystemInstance) GetResults(scanID int) ResultsStatistics {
var results ResultsStatistics
data, err := sendRequest(sys, http.MethodGet, fmt.Sprintf("/sast/scans/%v/resultsStatistics", scanID), nil, nil)
if err != nil {
sys.logger.Errorf("Failed to fetch scan results for scanID %v: %s", scanID, err)
return results
}
json.Unmarshal(data, &results)
return results
}
// RequestNewReport triggers the gereration of a report for a specific scan addressed by scanID
func (sys *SystemInstance) RequestNewReport(scanID int, reportType string) (bool, Report) {
report := Report{}
jsonData := map[string]interface{}{
"scanId": scanID,
"reportType": reportType,
"comment": "Scan report triggered by Piper",
}
jsonValue, _ := json.Marshal(jsonData)
header := http.Header{}
header.Set("cxOrigin", "GolangScript")
header.Set("Content-Type", "application/json")
data, err := sendRequest(sys, http.MethodPost, "/reports/sastScan", bytes.NewBuffer(jsonValue), header)
if err != nil {
sys.logger.Errorf("Failed to trigger report generation for scan %v: %s", scanID, err)
return false, report
}
json.Unmarshal(data, &report)
return true, report
}
// GetReportStatus returns the status of the report generation process
func (sys *SystemInstance) GetReportStatus(reportID int) ReportStatusResponse {
var response ReportStatusResponse
header := http.Header{}
header.Set("Accept", "application/json")
data, err := sendRequest(sys, http.MethodGet, fmt.Sprintf("/reports/sastScan/%v/status", reportID), nil, header)
if err != nil {
sys.logger.Errorf("Failed to fetch report status for reportID %v: %s", reportID, err)
return response
}
json.Unmarshal(data, &response)
return response
}
// DownloadReport downloads the report addressed by reportID and returns the XML contents
func (sys *SystemInstance) DownloadReport(reportID int) (bool, []byte) {
header := http.Header{}
header.Set("Accept", "application/json")
data, err := sendRequest(sys, http.MethodGet, fmt.Sprintf("/reports/sastScan/%v", reportID), nil, header)
if err != nil {
sys.logger.Errorf("Failed to download report with reportID %v: %s", reportID, err)
return false, []byte{}
}
return true, data
}
// FilterTeamByName filters a team by its name
func (sys *SystemInstance) FilterTeamByName(teams []Team, teamName string) Team {
for _, team := range teams {
if team.FullName == teamName {
return team
}
}
return Team{}
}
// FilterTeamByID filters a team by its ID
func (sys *SystemInstance) FilterTeamByID(teams []Team, teamID string) Team {
for _, team := range teams {
if team.ID == teamID {
return team
}
}
return Team{}
}
// FilterProjectByName filters a project by its name
func (sys *SystemInstance) FilterProjectByName(projects []Project, projectName string) Project {
for _, project := range projects {
if project.Name == projectName {
sys.logger.Debugf("Filtered project with name %v", project.Name)
return project
}
}
return Project{}
}
// FilterPresetByName filters a preset by its name
func (sys *SystemInstance) FilterPresetByName(presets []Preset, presetName string) Preset {
for _, preset := range presets {
if preset.Name == presetName {
return preset
}
}
return Preset{}
}
// FilterPresetByID filters a preset by its name
func (sys *SystemInstance) FilterPresetByID(presets []Preset, presetID int) Preset {
for _, preset := range presets {
if preset.ID == presetID {
return preset
}
}
return Preset{}
}