From 862812c4635ded3647f3e7b76e2807de06c652ba Mon Sep 17 00:00:00 2001 From: Salim Afiune Maya Date: Tue, 19 May 2020 13:19:20 -0600 Subject: [PATCH] feat(api): add compliance service This new `ComplianceService` is a service that interacts with the compliance endpoints from the Lacework Server, as well as the reports endpoints. The new api functions available for the Compliance Service are: * `ListGcpProject` * `GetAwsReport` * `DownloadAwsReportPDF` * `RunAwsReport` * `ListAzureSubscription` * `GetAzureReport` * `DownloadAzureReportPDF` * `RunAzureReport` * `GetGcpReport` * `DownloadGcpReportPDF` * `RunGcpReport` Signed-off-by: Salim Afiune Maya --- api/api.go | 10 +++ api/client.go | 2 + api/compliance.go | 99 +++++++++++++++++++++++++ api/compliance_aws.go | 117 +++++++++++++++++++++++++++++ api/compliance_azure.go | 159 ++++++++++++++++++++++++++++++++++++++++ api/compliance_gcp.go | 139 +++++++++++++++++++++++++++++++++++ 6 files changed, 526 insertions(+) create mode 100644 api/compliance.go create mode 100644 api/compliance_aws.go create mode 100644 api/compliance_azure.go create mode 100644 api/compliance_gcp.go diff --git a/api/api.go b/api/api.go index f151c6300..3e5ca38b5 100644 --- a/api/api.go +++ b/api/api.go @@ -36,6 +36,16 @@ const ( apiVulnerabilitiesReportFromID = "external/vulnerabilities/container/imageId/%s" apiVulnerabilitiesReportFromDigest = "external/vulnerabilities/container/imageDigest/%s" + apiComplianceAwsLatestReport = "external/compliance/aws/GetLatestComplianceReport?AWS_ACCOUNT_ID=%s" + apiComplianceGcpLatestReport = "external/compliance/gcp/GetLatestComplianceReport?GCP_ORG_ID=%s&GCP_PROJ_ID=%s" + apiComplianceGcpListProjects = "external/compliance/gcp/ListProjectsForOrganization?GCP_ORG_ID=%s" + apiComplianceAzureLatestReport = "external/compliance/azure/GetLatestComplianceReport?AZURE_TENANT_ID=%s&AZURE_SUBS_ID=%s" + apiComplianceAzureListSubscriptions = "external/compliance/azure/ListSubscriptionsForTenant?AZURE_TENANT_ID=%s" + + apiRunReportGcp = "external/runReport/gcp/%s" + apiRunReportAws = "external/runReport/aws/%s" + apiRunReportAzure = "external/runReport/azure/%s" + apiEventsDetails = "external/events/GetEventDetails" apiEventsDateRange = "external/events/GetEventsForDateRange" ) diff --git a/api/client.go b/api/client.go index 7639eb4f4..867933bd8 100644 --- a/api/client.go +++ b/api/client.go @@ -43,6 +43,7 @@ type Client struct { log *zap.Logger Events *EventsService + Compliance *ComplianceService Integrations *IntegrationsService Vulnerabilities *VulnerabilitiesService } @@ -86,6 +87,7 @@ func NewClient(account string, opts ...Option) (*Client, error) { c: &http.Client{Timeout: defaultTimeout}, } c.Events = &EventsService{c} + c.Compliance = &ComplianceService{c} c.Integrations = &IntegrationsService{c} c.Vulnerabilities = &VulnerabilitiesService{c} diff --git a/api/compliance.go b/api/compliance.go new file mode 100644 index 000000000..4de056e57 --- /dev/null +++ b/api/compliance.go @@ -0,0 +1,99 @@ +// +// Author:: Salim Afiune Maya () +// Copyright:: Copyright 2020, Lacework Inc. +// License:: Apache License, Version 2.0 +// +// 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 api + +import "fmt" + +// ComplianceService is a service that interacts with the compliance +// endpoints from the Lacework Server +type ComplianceService struct { + client *Client +} + +func (svc *ComplianceService) ListGcpProjects(orgID string) ( + response compGcpProjectsResponse, + err error, +) { + apiPath := fmt.Sprintf(apiComplianceGcpListProjects, orgID) + err = svc.client.RequestDecoder("GET", apiPath, nil, &response) + return +} + +type compGcpProjectsResponse struct { + Data []CompGcpProjects `json:"data"` + Ok bool `json:"ok"` + Message string `json:"message"` +} + +type CompGcpProjects struct { + Organization string `json:"organization"` + Projects []string `json:"projects"` +} + +type ComplianceSummary struct { + AssessedResourceCount int `json:"assessed_resource_count"` + NumCompliant int `json:"num_compliant"` + NumNotCompliant int `json:"num_not_compliant"` + NumRecommendations int `json:"num_recommendations"` + NumSeverity1NonCompliance int `json:"num_severity_1_non_compliance"` + NumSeverity2NonCompliance int `json:"num_severity_2_non_compliance"` + NumSeverity3NonCompliance int `json:"num_severity_3_non_compliance"` + NumSeverity4NonCompliance int `json:"num_severity_4_non_compliance"` + NumSeverity5NonCompliance int `json:"num_severity_5_non_compliance"` + NumSuppressed int `json:"num_suppressed"` + SuppressedResourceCount int `json:"suppressed_resource_count"` + ViolatedResourceCount int `json:"violated_resource_count"` +} + +type ComplianceRecommendation struct { + RecID string `json:"rec_id"` + AssessedResourceCount int `json:"assessed_resource_count"` + ResourceCount int `json:"resource_count"` + Category string `json:"category"` + InfoLink string `json:"info_link"` + Service string `json:"service"` + Severity int `json:"severity"` + Status string `json:"status"` + Suppressions []string `json:"suppressions"` + Title string `json:"title"` + Violations []ComplianceViolation `json:"violations"` +} + +func (r *ComplianceRecommendation) SeverityString() string { + switch r.Severity { + case 1: + return "Critical" + case 2: + return "High" + case 3: + return "Medium" + case 4: + return "Low" + case 5: + return "Info" + default: + return "Unknown" + } +} + +type ComplianceViolation struct { + Region string `json:"region"` + Resource string `json:"resource"` + Reasons []string `json:"reasons"` +} diff --git a/api/compliance_aws.go b/api/compliance_aws.go new file mode 100644 index 000000000..6af3c3a2e --- /dev/null +++ b/api/compliance_aws.go @@ -0,0 +1,117 @@ +// +// Author:: Salim Afiune Maya () +// Copyright:: Copyright 2020, Lacework Inc. +// License:: Apache License, Version 2.0 +// +// 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 api + +import ( + "fmt" + "io" + "os" + "time" + + "github.com/pkg/errors" +) + +type ComplianceAwsReportConfig struct { + AccountID string + Type string +} + +func (svc *ComplianceService) GetAwsReport(config ComplianceAwsReportConfig) ( + response complianceAwsReportResponse, + err error, +) { + if config.AccountID == "" { + err = errors.New("account_id is required") + return + } + apiPath := fmt.Sprintf(apiComplianceAwsLatestReport, config.AccountID) + + if config.Type != "" { + apiPath = fmt.Sprintf("%s&REPORT_TYPE=%s", apiPath, config.Type) + } + + // add JSON format, if not, the default is PDF + apiPath = fmt.Sprintf("%s&FILE_FORMAT=json", apiPath) + err = svc.client.RequestDecoder("GET", apiPath, nil, &response) + return +} + +func (svc *ComplianceService) DownloadAwsReportPDF(filepath string, config ComplianceAwsReportConfig) error { + if config.AccountID == "" { + return errors.New("account_id is required") + } + + apiPath := fmt.Sprintf(apiComplianceAwsLatestReport, config.AccountID) + + if config.Type != "" { + apiPath = fmt.Sprintf("%s&REPORT_TYPE=%s", apiPath, config.Type) + } + + request, err := svc.client.NewRequest("GET", apiPath, nil) + if err != nil { + return err + } + + response, err := svc.client.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + + err = checkErrorInResponse(response) + if err != nil { + return err + } + + // Create the file + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + // Write the body to file + _, err = io.Copy(out, response.Body) + return err +} + +func (svc *ComplianceService) RunAwsReport(accountID string) ( + response map[string]interface{}, // @afiune not consistent with the other cloud providers + err error, +) { + apiPath := fmt.Sprintf(apiRunReportAws, accountID) + err = svc.client.RequestDecoder("POST", apiPath, nil, &response) + return +} + +type complianceAwsReportResponse struct { + Data []ComplianceAwsReport `json:"data"` + Ok bool `json:"ok"` + Message string `json:"message"` +} + +type ComplianceAwsReport struct { + ReportTitle string `json:"reportTitle"` + ReportType string `json:"reportType"` + ReportTime time.Time `json:"reportTime"` + AccountID string `json:"accountId"` + AccountAlias string `json:"accountAlias"` + Summary []ComplianceSummary `json:"summary"` + Recommendations []ComplianceRecommendation `json:"recommendations"` +} diff --git a/api/compliance_azure.go b/api/compliance_azure.go new file mode 100644 index 000000000..ae11095d6 --- /dev/null +++ b/api/compliance_azure.go @@ -0,0 +1,159 @@ +// +// Author:: Salim Afiune Maya () +// Copyright:: Copyright 2020, Lacework Inc. +// License:: Apache License, Version 2.0 +// +// 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 api + +import ( + "fmt" + "io" + "os" + "time" + + "github.com/pkg/errors" +) + +type ComplianceAzureReportConfig struct { + TenantID string + SubscriptionID string + Type string +} + +func (svc *ComplianceService) ListAzureSubscriptions(tenantID string) ( + response compAzureSubsResponse, + err error, +) { + apiPath := fmt.Sprintf(apiComplianceAzureListSubscriptions, tenantID) + err = svc.client.RequestDecoder("GET", apiPath, nil, &response) + return +} + +func (svc *ComplianceService) GetAzureReport(config ComplianceAzureReportConfig) ( + response complianceAzureReportResponse, + err error, +) { + if config.TenantID == "" || config.SubscriptionID == "" { + err = errors.New("tenant_id and subscription_id are required") + return + } + apiPath := fmt.Sprintf(apiComplianceAzureLatestReport, config.TenantID, config.SubscriptionID) + + if config.Type != "" { + apiPath = fmt.Sprintf("%s&REPORT_TYPE=%s", apiPath, config.Type) + } + + // add JSON format, if not, the default is PDF + apiPath = fmt.Sprintf("%s&FILE_FORMAT=json", apiPath) + err = svc.client.RequestDecoder("GET", apiPath, nil, &response) + return +} + +func (svc *ComplianceService) DownloadAzureReportPDF(filepath string, config ComplianceAzureReportConfig) error { + if config.TenantID == "" || config.SubscriptionID == "" { + return errors.New("tenant_id and subscription_id are required") + } + + apiPath := fmt.Sprintf(apiComplianceAzureLatestReport, config.TenantID, config.SubscriptionID) + + if config.Type != "" { + apiPath = fmt.Sprintf("%s&REPORT_TYPE=%s", apiPath, config.Type) + } + + request, err := svc.client.NewRequest("GET", apiPath, nil) + if err != nil { + return err + } + + response, err := svc.client.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + + err = checkErrorInResponse(response) + if err != nil { + return err + } + + // Create the file + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + // Write the body to file + _, err = io.Copy(out, response.Body) + return err +} + +func (svc *ComplianceService) RunAzureReport(tenantID string) ( + response complianceRunAzureReportResponse, + err error, +) { + apiPath := fmt.Sprintf(apiRunReportAzure, tenantID) + err = svc.client.RequestDecoder("POST", apiPath, nil, &response) + return +} + +type complianceRunAzureReportResponse struct { + IntgGuid string `json:"intgGuid"` + MultiContextEvalGuid string `json:"multiContextEvalGuid"` + EvalRequests []complianceAzureEvalRequest `json:"evalRequests"` +} + +type complianceAzureEvalRequest struct { + EvalGuid string `json:"evalGuid"` + EvalCtx complianceRunReportAzureContex `json:"evalCtx"` + SubmitErrorMsg interface{} `json:"submitErrorMsg"` +} + +type complianceRunReportAzureContex struct { + SubscriptionID string `json:"subscriptionId"` + SubscriptionName string `json:"subscriptionName"` + TenantID string `json:"tenantId"` + TenantName string `json:"tenantName"` +} + +type compAzureSubsResponse struct { + Data []CompAzureSubscriptions `json:"data"` + Ok bool `json:"ok"` + Message string `json:"message"` +} + +type CompAzureSubscriptions struct { + Tenant string `json:"tenant"` + Subscriptions []string `json:"subscriptions"` +} + +type complianceAzureReportResponse struct { + Data []ComplianceAzureReport `json:"data"` + Ok bool `json:"ok"` + Message string `json:"message"` +} + +type ComplianceAzureReport struct { + ReportTitle string `json:"reportTitle"` + ReportType string `json:"reportType"` + ReportTime time.Time `json:"reportTime"` + TenantID string `json:"tenantId"` + TenantName string `json:"tenantName"` + SubscriptionID string `json:"subscriptionId"` + SubscriptionName string `json:"subscriptionName"` + Summary []ComplianceSummary `json:"summary"` + Recommendations []ComplianceRecommendation `json:"recommendations"` +} diff --git a/api/compliance_gcp.go b/api/compliance_gcp.go new file mode 100644 index 000000000..01ffe8736 --- /dev/null +++ b/api/compliance_gcp.go @@ -0,0 +1,139 @@ +// +// Author:: Salim Afiune Maya () +// Copyright:: Copyright 2020, Lacework Inc. +// License:: Apache License, Version 2.0 +// +// 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 api + +import ( + "fmt" + "io" + "os" + "time" + + "github.com/pkg/errors" +) + +type ComplianceGcpReportConfig struct { + OrganizationID string + ProjectID string + Type string +} + +func (svc *ComplianceService) GetGcpReport(config ComplianceGcpReportConfig) ( + response complianceGcpReportResponse, + err error, +) { + if config.OrganizationID == "" || config.ProjectID == "" { + err = errors.New("organization_id and project_id is required") + return + } + apiPath := fmt.Sprintf(apiComplianceGcpLatestReport, config.OrganizationID, config.ProjectID) + + if config.Type != "" { + apiPath = fmt.Sprintf("%s&REPORT_TYPE=%s", apiPath, config.Type) + } + + // add JSON format, if not, the default is PDF + apiPath = fmt.Sprintf("%s&FILE_FORMAT=json", apiPath) + err = svc.client.RequestDecoder("GET", apiPath, nil, &response) + return +} + +func (svc *ComplianceService) DownloadGcpReportPDF(filepath string, config ComplianceGcpReportConfig) error { + if config.OrganizationID == "" || config.ProjectID == "" { + return errors.New("organization_id and project_id is required") + } + + apiPath := fmt.Sprintf(apiComplianceGcpLatestReport, config.OrganizationID, config.ProjectID) + + if config.Type != "" { + apiPath = fmt.Sprintf("%s&REPORT_TYPE=%s", apiPath, config.Type) + } + + request, err := svc.client.NewRequest("GET", apiPath, nil) + if err != nil { + return err + } + + response, err := svc.client.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + + err = checkErrorInResponse(response) + if err != nil { + return err + } + + // Create the file + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + // Write the body to file + _, err = io.Copy(out, response.Body) + return err +} + +func (svc *ComplianceService) RunGcpReport(projectID string) ( + response complianceRunGcpReportResponse, + err error, +) { + apiPath := fmt.Sprintf(apiRunReportGcp, projectID) + err = svc.client.RequestDecoder("POST", apiPath, nil, &response) + return +} + +type complianceRunGcpReportResponse struct { + IntgGuid string `json:"intgGuid"` + MultiContextEvalGuid string `json:"multiContextEvalGuid"` + EvalRequests []complianceGcpEvalRequest `json:"evalRequests"` +} + +type complianceGcpEvalRequest struct { + EvalGuid string `json:"evalGuid"` + EvalCtx complianceRunReportGcpContex `json:"evalCtx"` + SubmitErrorMsg interface{} `json:"submitErrorMsg"` +} + +type complianceRunReportGcpContex struct { + OrganizationID string `json:"organizationId"` + OrganizationName string `json:"organizationName"` + ProjectID string `json:"projectId"` + ProjectName string `json:"projectName"` +} + +type complianceGcpReportResponse struct { + Data []ComplianceGcpReport `json:"data"` + Ok bool `json:"ok"` + Message string `json:"message"` +} + +type ComplianceGcpReport struct { + ReportTitle string `json:"reportTitle"` + ReportType string `json:"reportType"` + ReportTime time.Time `json:"reportTime"` + OrganizationID string `json:"organizationId"` + OrganizationName string `json:"organizationName"` + ProjectID string `json:"projectId"` + ProjectName string `json:"projectName"` + Summary []ComplianceSummary `json:"summary"` + Recommendations []ComplianceRecommendation `json:"recommendations"` +}