Skip to content
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
6 changes: 4 additions & 2 deletions internal/args/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"errors"
"flag"
"fmt"
"os"
"strings"

"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks/naming"
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks/organization"
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks/performance"
Expand All @@ -13,8 +16,6 @@ import (
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/defaults"
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/types"
"github.com/spf13/viper"
"os"
"strings"
)

func ParseArgs(args []string) (*config.OctolintConfig, error) {
Expand Down Expand Up @@ -64,6 +65,7 @@ func ParseArgs(args []string) (*config.OctolintConfig, error) {
flags.IntVar(&octolintConfig.MaxInvalidNameTargets, "maxInvalidNameTargets", defaults.MaxInvalidNameTargets, "Maximum number of targets to check for invalid names for the "+naming.OctoLintInvalidTargetNames+" check. Set to 0 to check all targets.")
flags.IntVar(&octolintConfig.MaxInsecureK8sTargets, "maxInsecureK8sTargets", defaults.MaxInsecureK8sTargets, "Maximum number of targets to check for insecure k8s configuration for the "+security.OctoLintInsecureK8sTargets+" check. Set to 0 to check all targets.")
flags.IntVar(&octolintConfig.MaxDeploymentTasks, "maxDeploymentTasks", defaults.MaxDeploymentTasks, "Maximum number of deployment tasks to scan for the "+performance.OctoLintDeploymentQueuedTime+" check. Set to 0 to check all targets.")
flags.IntVar(&octolintConfig.MaxSha1CertificatesMachines, "maxSha1CertificatesMachines", defaults.MaxSha1CertificatesMachines, "Maximum number of machines to check for SHA1 certificates for the "+security.OctoLintSha1Certificates+" check. Set to 0 to check all targets and workers.")
flags.StringVar(&octolintConfig.ContainerImageRegex, "containerImageRegex", "", "The regular expression used to validate container images for the "+naming.OctoLintContainerImageName+" check")
flags.StringVar(&octolintConfig.VariableNameRegex, "variableNameRegex", "", "The regular expression used to validate variable names for the "+naming.OctoLintInvalidVariableNames+" check")
flags.StringVar(&octolintConfig.TargetNameRegex, "targetNameRegex", "", "The regular expression used to validate target names for the "+naming.OctoLintInvalidTargetNames+" check")
Expand Down
4 changes: 3 additions & 1 deletion internal/checks/factory/octopus_check_factory.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package factory

import (
"strings"

"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks"
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks/naming"
Expand All @@ -10,7 +12,6 @@ import (
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/config"
"github.com/samber/lo"
"golang.org/x/exp/slices"
"strings"
)

// OctopusCheckFactory builds all the lint checks. This is where you can customize things like error handlers.
Expand Down Expand Up @@ -45,6 +46,7 @@ func (o OctopusCheckFactory) BuildAllChecks(config *config.OctolintConfig) ([]ch
security.NewOctopusInsecureK8sCheck(o.client, config, o.errorHandler),
security.NewOctopusInsecureFeedsCheck(o.client, config, o.errorHandler),
security.NewOctopusInsecureSubscriptionsCheck(o.client, config, o.errorHandler),
security.NewOctopusSha1CertificatesCheck(o.client, config, o.errorHandler),
organization.NewOctopusEnvironmentCountCheck(o.client, config, o.errorHandler),
organization.NewOctopusDefaultProjectGroupCountCheck(o.client, config, o.errorHandler),
organization.NewOctopusEmptyProjectCheck(o.client, config, o.errorHandler),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ package security
import (
"errors"
"fmt"
"strings"

"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/newclient"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/resources"
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks"
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/config"
"go.uber.org/zap"
"strings"
)

// CustomProject is the simplest representation of a project and its version controlled settings
Expand Down Expand Up @@ -103,7 +104,7 @@ func (o OctopusDuplicatedGitCredentialsCheck) Execute(concurrency int) (checks.O
}

return checks.NewOctopusCheckResultImpl(
"No Git usernames have been resued",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆

"No Git usernames have been reused",
o.Id(),
"",
checks.Ok,
Expand Down
188 changes: 188 additions & 0 deletions internal/checks/security/octopus_sha1_certificates_check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package security

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
"strings"

"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/machines"
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks"
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/client_wrapper"
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/config"
"go.uber.org/zap"
)

const (
OctoLintSha1Certificates = "OctoLintSha1Certificates"
sha1Alg = "sha1RSA"
)

// OctopusSha1CertificatesCheck checks to see if any targets, workers or the server itself is using a sha1 certificate
type OctopusSha1CertificatesCheck struct {
client *client.Client
errorHandler checks.OctopusClientErrorHandler
config *config.OctolintConfig
}

type Sha1CertificateResult struct {
Name string
Type string // "Target", "Worker", or "Global"
}

type ServerCertificate struct {
ID string `json:"Id"`
Name string `json:"Name"`
Thumbprint string `json:"Thumbprint"`
SignatureAlgorithm string `json:"SignatureAlgorithm"`
Links map[string]string `json:"Links"`
}

func NewOctopusSha1CertificatesCheck(client *client.Client, config *config.OctolintConfig, errorHandler checks.OctopusClientErrorHandler) OctopusSha1CertificatesCheck {
return OctopusSha1CertificatesCheck{config: config, client: client, errorHandler: errorHandler}
}

func (o OctopusSha1CertificatesCheck) Id() string {
return OctoLintSha1Certificates
}

// fetchServerCertificate gets the server certificate object and returns it.
func fetchServerCertificate(url, apiKey, accessToken string) (*ServerCertificate, error) {
requestURL := fmt.Sprintf("%s/api/configuration/certificates/certificate-global", url)

req, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil {
return nil, err
}

if apiKey != "" {
req.Header.Set("X-Octopus-ApiKey", apiKey)
} else if accessToken != "" {
req.Header.Set("Authorization", "Bearer "+accessToken)
}

res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}

var cert ServerCertificate
if err := json.NewDecoder(res.Body).Decode(&cert); err != nil {
return nil, err
}

return &cert, nil
}

// hasSha1Certificate checks if an endpoint has CertificateSignatureAlgorithm == sha1Alg
func hasSha1Certificate(ep machines.IEndpoint) bool {
if ep == nil {
return false
}

switch e := ep.(type) {
case *machines.ListeningTentacleEndpoint:
return e.CertificateSignatureAlgorithm == sha1Alg
case *machines.PollingTentacleEndpoint:
return e.CertificateSignatureAlgorithm == sha1Alg
default:
return false
}
}

// addSha1FromMachines is a top-level generic helper (function literals can't have type params).
func addSha1FromMachines[T any](
results *[]Sha1CertificateResult,
items []T,
getName func(T) string,
getEndpoint func(T) machines.IEndpoint,
typ string,
) {
for _, item := range items {
if hasSha1Certificate(getEndpoint(item)) {
*results = append(*results, Sha1CertificateResult{Name: getName(item), Type: typ})
}
}
}

func (o OctopusSha1CertificatesCheck) Execute(concurrency int) (checks.OctopusCheckResult, error) {
if o.client == nil {
return nil, errors.New("octoclient is nil")
}

zap.L().Debug("Starting check " + o.Id())
defer func() {
zap.L().Debug("Ended check " + o.Id())
}()

var results []Sha1CertificateResult

// Check server certificate
cert, err := fetchServerCertificate(o.config.Url, o.config.ApiKey, o.config.AccessToken)
if err != nil {
return o.errorHandler.HandleError(o.Id(), checks.Security, err)
}
if cert != nil && cert.SignatureAlgorithm == sha1Alg {
results = append(results, Sha1CertificateResult{Name: cert.Name, Type: "Global"})
}

// Check deployment targets
targets, err := client_wrapper.GetMachines(o.config.MaxSha1CertificatesMachines, o.client, o.client.GetSpaceID())
if err != nil {
return o.errorHandler.HandleError(o.Id(), checks.Security, err)
}
addSha1FromMachines(&results, targets,
func(m *machines.DeploymentTarget) string { return m.Name },
func(m *machines.DeploymentTarget) machines.IEndpoint { return m.Endpoint },
"Target",
)

// Check workers
workers, err := client_wrapper.GetWorkers(o.config.MaxSha1CertificatesMachines, o.client, o.client.GetSpaceID())
if err != nil {
return o.errorHandler.HandleError(o.Id(), checks.Security, err)
}
addSha1FromMachines(&results, workers,
func(w *machines.Worker) string { return w.Name },
func(w *machines.Worker) machines.IEndpoint { return w.Endpoint },
"Worker",
)

// Provide results
if len(results) > 0 {
// Sort by Type then Name for stable output
sort.Slice(results, func(i, j int) bool {
if results[i].Type == results[j].Type {
return results[i].Name < results[j].Name
}
return results[i].Type < results[j].Type
})

lines := make([]string, len(results))
for i, m := range results {
lines[i] = fmt.Sprintf("%s: %s", m.Type, m.Name)
}

return checks.NewOctopusCheckResultImpl(
"The following resources use a SHA1 certificate:\n"+strings.Join(lines, "\n"),
o.Id(),
"",
checks.Warning,
checks.Security), nil
}

return checks.NewOctopusCheckResultImpl(
"There are no uses of SHA1 certificates in targets, workers or the main Server Certificate",
o.Id(),
"",
checks.Ok,
checks.Security), nil
}
61 changes: 61 additions & 0 deletions internal/checks/security/octopus_sha1_certificates_check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package security

import (
"errors"
"path/filepath"
"testing"

"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks"
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/config"
"github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/octoclient"
"github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/test"
)

func TestSha1Certificates(t *testing.T) {
testFramework := test.OctopusContainerTest{}

testFramework.ArrangeTest(t, func(t *testing.T, container *test.OctopusContainer, client *client.Client) error {
// Act: Deploy Terraform scenario that sets up SHA1 certificates
newSpaceId, err := testFramework.Act(
t,
container,
filepath.Join("..", "..", "..", "test", "terraform"),
"33-sha1certificates", // folder containing your Terraform scenario
[]string{},
)
if err != nil {
return err
}

// Create a client for the new space
newSpaceClient, err := octoclient.CreateClient(container.URI, newSpaceId, test.ApiKey)
if err != nil {
return err
}

// Create the check
check := NewOctopusSha1CertificatesCheck(
newSpaceClient,
&config.OctolintConfig{
Url: container.URI,
ApiKey: test.ApiKey,
MaxSha1CertificatesMachines: 100,
},
checks.OctopusClientPermissiveErrorHandler{},
)

// Execute the check
result, err := check.Execute(2)
if err != nil {
return err
}

// Assert
if result == nil || result.Severity() != checks.Ok {
return errors.New("check should have passed")
}

return nil
})
}
23 changes: 23 additions & 0 deletions internal/client_wrapper/workers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package client_wrapper

import (
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/machines"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/newclient"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/workers"
)

func GetWorkers(limit int, client newclient.Client, spaceID string) ([]*machines.Worker, error) {
if limit == 0 {
return workers.GetAll(client, spaceID)
}

result, err := workers.Get(client, spaceID, machines.WorkersQuery{
Take: limit,
})

if err != nil {
return nil, err
}

return result.Items, nil
}
1 change: 1 addition & 0 deletions internal/config/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type OctolintConfig struct {
MaxInvalidNameTargets int
MaxInsecureK8sTargets int
MaxDeploymentTasks int
MaxSha1CertificatesMachines int
}

type StringSliceArgs []string
Expand Down
1 change: 1 addition & 0 deletions internal/defaults/defaultValues.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ const MaxTenantTagsTenants = 100
const MaxProjectSpecificEnvironmentEnvironments = 100
const MaxInvalidNameTargets = 100
const MaxInsecureK8sTargets = 100
const MaxSha1CertificatesMachines = 100
const MaxDeploymentTasks = 100
const MaxDefaultStepNameProjects = 100
5 changes: 5 additions & 0 deletions test/terraform/33-sha1certificates/config.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
terraform {
required_providers {
octopusdeploy = { source = "OctopusDeployLabs/octopusdeploy", version = "0.30.4" }
}
}
20 changes: 20 additions & 0 deletions test/terraform/33-sha1certificates/environments.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
resource "octopusdeploy_environment" "development_environment" {
allow_dynamic_infrastructure = true
description = "A development environment"
name = "Development"
use_guided_failure = false
}

resource "octopusdeploy_environment" "test_environment" {
allow_dynamic_infrastructure = true
description = "A test environment"
name = "Test"
use_guided_failure = false
}

resource "octopusdeploy_environment" "production_environment" {
allow_dynamic_infrastructure = true
description = "A production environment"
name = "Production"
use_guided_failure = false
}
Loading