Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd: adds gokakashi scan command #110

Merged
merged 3 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions cmd/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var (
server string
token string
workspace string
name string
)

//ToDo: for any table status which results to error should we upload err message or just status error
Expand All @@ -50,7 +51,7 @@ func agentRegister(cmd *cobra.Command, args []string) {
// log.Printf("Server: %s, Token: %s, Workspace: %s", server, token, workspace)

// Register the agent
agentID, err := registerAgent(server, token, workspace)
agentID, err := registerAgent(server, token, workspace, name)
if err != nil {
log.Fatalf("Failed to register the agent: %v", err)
}
Expand All @@ -61,11 +62,12 @@ func agentRegister(cmd *cobra.Command, args []string) {
pollTasks(server, token, agentID, workspace)
}

func registerAgent(server, token, workspace string) (int, error) {
func registerAgent(server, token, workspace, name string) (int, error) {
reqBody := agents.RegisterAgentRequest{
Server: server,
Token: token,
Workspace: workspace,
Name: name,
}
reqBodyJSON, _ := json.Marshal(reqBody)

Expand Down Expand Up @@ -227,9 +229,16 @@ func processTask(server, token string, task agenttasks.GetAgentTaskResponse, wor

// step 6: Verify scans.Notify field exist
// Todo: if exists update the status to notify_pending else complete
err = updateScanStatus(server, token, scan.ID, "notify_pending")
if err != nil {
log.Printf("Failed to update scan status to 'scan_in_progress': %v", err)
if scan.Notify == nil || len(*scan.Notify) == 0 {
log.Printf("No notify specified for scan ID: %s", scan.ID)
if err := updateScanStatus(server, token, scan.ID, "success"); err != nil {
log.Printf("Failed to update scan status to 'success': %v", err)
}
} else {
err = updateScanStatus(server, token, scan.ID, "notify_pending")
if err != nil {
log.Printf("Failed to update scan status to 'scan_in_progress': %v", err)
}
}

if err := updateAgentTaskStatus(server, token, task.ID, agentID, "complete"); err != nil {
Expand Down
17 changes: 15 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,27 @@ func init() {
serverConfigFilePath = serverCmd.Flags().String("config", "", "Path to the config YAML file")
rootCmd.AddCommand(serverCmd)

rootCmd.AddCommand(scanCmd)

agentStartCmd.Flags().StringVar(&server, "server", "", "The server address to connect to")
agentStartCmd.Flags().StringVar(&token, "token", "", "Authentication token for the server")
agentStartCmd.Flags().StringVar(&workspace, "workspace", "", "Path to the local workspace")
rootCmd.AddCommand(agentCmd)
agentCmd.AddCommand(agentStartCmd)

// Flags for the `scan image` command
scanImageCmd.Flags().StringVar(&image, "image", "", "Container image to scan")
scanImageCmd.Flags().StringVar(&policyName, "policy", "", "Policy name for the scan")
scanImageCmd.Flags().StringVar(&server, "server", "", "The server address to connect to")
scanImageCmd.Flags().StringVar(&token, "token", "", "Authentication token for the server")

// Flags for the `scan status` command
scanStatusCmd.Flags().StringVar(&scanID, "scanID", "", "Scan ID to check status")
scanStatusCmd.Flags().StringVar(&server, "server", "", "The server address to connect to")
scanStatusCmd.Flags().StringVar(&token, "token", "", "Authentication token for the server")

rootCmd.AddCommand(scanCmd)
scanCmd.AddCommand(scanImageCmd)
scanCmd.AddCommand(scanStatusCmd)

}

func Execute() {
Expand Down
193 changes: 193 additions & 0 deletions cmd/scan.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,207 @@
package cmd

import (
"bytes"
"encoding/json"
"fmt"
"github.com/shinobistack/gokakashi/internal/restapi/v1/integrations"
"github.com/shinobistack/gokakashi/internal/restapi/v1/policies"
"github.com/shinobistack/gokakashi/internal/restapi/v1/scans"
"github.com/spf13/cobra"
"log"
"net/http"
"net/url"
"strings"
)

var scanCmd = &cobra.Command{
Use: "scan",
Short: "Scan a container image",
}

var scanImageCmd = &cobra.Command{
Use: "image",
Short: "Trigger a scan for a container image",
Run: scanImage,
}

var scanStatusCmd = &cobra.Command{
Use: "status",
Short: "Get the status of a scan",
Run: getScanStatus,
}

var (
image string
policyName string
scanID string
// server and token are Defined in agent.go
)

func normalizeServer(server string) string {
if !strings.HasPrefix(server, "http://") && !strings.HasPrefix(server, "https://") {
server = "http://" + server // Default to HTTP
}
return server
}

func constructURL(server string, port int, path string) string {
base := normalizeServer(server)
u, err := url.Parse(base)
if err != nil {
log.Fatalf("Invalid server URL: %s", base)
}
if u.Port() == "" {
u.Host = fmt.Sprintf("%s:%d", u.Host, port)
}
u.Path = path
return u.String()
}

func scanImage(cmd *cobra.Command, args []string) {

if server == "" || token == "" || image == "" || policyName == "" {
log.Fatalf("Error: Missing required inputs. Please provide --server, --token, and --workspace.")
}

policy, err := fetchPolicyByName(server, 0, token, policyName)
if err != nil {
log.Fatalf("Failed to fetch policy: %v", err)
}
log.Println(policy)

integration, err := fetchIntegrationByName(server, 0, token, policy.Image.Registry)
if err != nil {
log.Fatalf("Failed to fetch integration: %v", err)
}

log.Println(integration)

reqBody := map[string]interface{}{
"policy_id": policy.ID,
"image": image,
"scanner": policy.Scanner,
"integration_id": integration.ID,
"notify": policy.Notify,
"status": "scan_pending",
}
log.Println(reqBody)

reqBodyJSON, _ := json.Marshal(reqBody)
url := constructURL(server, 0, "/api/v1/scans")

req, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBodyJSON))

if err != nil {
log.Fatalf("Failed to create scan request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalf("Failed to send scan request: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
log.Fatalf("Server responded with status: %d", resp.StatusCode)
}
log.Println(resp)
var response scans.CreateScanResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
log.Fatalf("Failed to decode scan response: %v", err)
}

log.Printf("Scan triggered successfully! Scan ID: %s, status: %s", response.ID, response.Status)
}

func getScanStatus(cmd *cobra.Command, args []string) {
if server == "" || token == "" || scanID == "" {
log.Fatalf("Error: Missing required inputs. Please provide --server, --token, and --workspace.")
}

url := constructURL(server, 0, fmt.Sprintf("/api/v1/scans/%s", scanID))

req, err := http.NewRequest("GET", url, nil)

if err != nil {
log.Fatalf("Failed to create status request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+token)

resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalf("Failed to send status request: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
log.Fatalf("Server responded with status: %d", resp.StatusCode)
}

var response struct {
Status string `json:"status"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
log.Fatalf("Failed to decode status response: %v", err)
}

log.Printf("Scan status: %s", response.Status)
}

func fetchPolicyByName(server string, port int, token string, policyName string) (*policies.GetPolicyResponse, error) {
url := constructURL(server, port, "/api/v1/policies") + fmt.Sprintf("?name=%s", policyName)

req, err := http.NewRequest("GET", url, nil)

if err != nil {
return nil, fmt.Errorf("failed to create policy fetch request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch policy: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("server responded with status: %d", resp.StatusCode)
}

var policy []policies.GetPolicyResponse
if err := json.NewDecoder(resp.Body).Decode(&policy); err != nil {
return nil, fmt.Errorf("failed to decode policy response: %w", err)
}

return &policy[0], nil
}

func fetchIntegrationByName(server string, port int, token string, integrationName string) (*integrations.GetIntegrationResponse, error) {
url := constructURL(server, port, "/api/v1/integrations") + fmt.Sprintf("?name=%s", integrationName)

req, err := http.NewRequest("GET", url, nil)

if err != nil {
return nil, fmt.Errorf("failed to create integration fetch request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch integration: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("server responded with status: %d", resp.StatusCode)
}

var integration []integrations.GetIntegrationResponse
if err := json.NewDecoder(resp.Body).Decode(&integration); err != nil {
return nil, fmt.Errorf("failed to decode integration response: %w", err)
}

return &integration[0], nil
}
2 changes: 2 additions & 0 deletions internal/assigner/assigner.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"time"
)

// Assigns scanID to available Agents

func normalizeServer(server string) string {
if !strings.HasPrefix(server, "http://") && !strings.HasPrefix(server, "https://") {
server = "http://" + server // Default to HTTP
Expand Down
11 changes: 10 additions & 1 deletion internal/notifier/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func NotifyProcess(server string, port int, token string) {
// Evaluate the 'when' condition using ReportParser
matched, severities, err := parser.ReportParser(notify.When, &scan)
if err != nil {
log.Printf("Error evaluating notify condition: %v", err)
log.Printf("Error evaluating notify.when:%v, %v", notify.When, err)
continue
}

Expand Down Expand Up @@ -149,6 +149,15 @@ func NotifyProcess(server string, port int, token string) {
}
}
}
if !matched {
log.Printf("Condition not matched for scanID: %s and image: %s. Updating status to success.", scan.ID, scan.Image)
err = updateScanStatus(server, port, token, scan.ID, "success")
if err != nil {
log.Printf("Failed to update status for scanID: %s: %v", scan.ID, err)
}
continue
}

}
}
}
Expand Down
16 changes: 13 additions & 3 deletions internal/restapi/v1/integrations/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"github.com/google/uuid"
"github.com/shinobistack/gokakashi/ent"
"github.com/shinobistack/gokakashi/ent/integrations"
"github.com/swaggest/usecase/status"
)

Expand All @@ -20,6 +21,10 @@ type GetIntegrationResponse struct {
Config map[string]interface{} `json:"config"`
}

type ListGetIntegrationRequests struct {
Name string `query:"name"`
}

type ListIntegrationResponse struct {
Integrations []GetIntegrationResponse `json:"integrations"`
}
Expand Down Expand Up @@ -50,9 +55,14 @@ func GetIntegration(client *ent.Client) func(ctx context.Context, req GetIntegra
}
}

func ListIntegrations(client *ent.Client) func(ctx context.Context, req struct{}, res *[]GetIntegrationResponse) error {
return func(ctx context.Context, req struct{}, res *[]GetIntegrationResponse) error {
integrations, err := client.Integrations.Query().All(ctx)
func ListIntegrations(client *ent.Client) func(ctx context.Context, req ListGetIntegrationRequests, res *[]GetIntegrationResponse) error {
return func(ctx context.Context, req ListGetIntegrationRequests, res *[]GetIntegrationResponse) error {
query := client.Integrations.Query()
if req.Name != "" {
query = query.Where(integrations.Name(req.Name))
}

integrations, err := query.All(ctx)
if err != nil {
return status.Wrap(errors.New("failed to fetch integrations"), status.Internal)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/restapi/v1/integrations/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func TestListIntegrations(t *testing.T) {
assert.NoError(t, err)

// Test case: List integrations
req := struct{}{}
req := integrations.ListGetIntegrationRequests{}
var res []integrations.GetIntegrationResponse
handler := integrations.ListIntegrations(client)
err = handler(context.Background(), req, &res)
Expand All @@ -88,10 +88,10 @@ func TestListIntegrations_EmptyDB(t *testing.T) {

// Prepare response
var res []integrations.GetIntegrationResponse

req := integrations.ListGetIntegrationRequests{}
// Execute ListIntegrations handler
handler := integrations.ListIntegrations(client)
err := handler(context.Background(), struct{}{}, &res)
err := handler(context.Background(), req, &res)

// Validate response
assert.NoError(t, err)
Expand Down
Loading
Loading