Skip to content

Commit 688ccd2

Browse files
Create new check for SHA1-certificates
1 parent f497816 commit 688ccd2

File tree

13 files changed

+357
-3
lines changed

13 files changed

+357
-3
lines changed

internal/args/args.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import (
55
"errors"
66
"flag"
77
"fmt"
8+
"os"
9+
"strings"
10+
811
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks/naming"
912
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks/organization"
1013
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks/performance"
@@ -13,8 +16,6 @@ import (
1316
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/defaults"
1417
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/types"
1518
"github.com/spf13/viper"
16-
"os"
17-
"strings"
1819
)
1920

2021
func ParseArgs(args []string) (*config.OctolintConfig, error) {
@@ -64,6 +65,7 @@ func ParseArgs(args []string) (*config.OctolintConfig, error) {
6465
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.")
6566
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.")
6667
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.")
68+
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.")
6769
flags.StringVar(&octolintConfig.ContainerImageRegex, "containerImageRegex", "", "The regular expression used to validate container images for the "+naming.OctoLintContainerImageName+" check")
6870
flags.StringVar(&octolintConfig.VariableNameRegex, "variableNameRegex", "", "The regular expression used to validate variable names for the "+naming.OctoLintInvalidVariableNames+" check")
6971
flags.StringVar(&octolintConfig.TargetNameRegex, "targetNameRegex", "", "The regular expression used to validate target names for the "+naming.OctoLintInvalidTargetNames+" check")

internal/checks/factory/octopus_check_factory.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package factory
22

33
import (
4+
"strings"
5+
46
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
57
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks"
68
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks/naming"
@@ -10,7 +12,6 @@ import (
1012
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/config"
1113
"github.com/samber/lo"
1214
"golang.org/x/exp/slices"
13-
"strings"
1415
)
1516

1617
// OctopusCheckFactory builds all the lint checks. This is where you can customize things like error handlers.
@@ -45,6 +46,7 @@ func (o OctopusCheckFactory) BuildAllChecks(config *config.OctolintConfig) ([]ch
4546
security.NewOctopusInsecureK8sCheck(o.client, config, o.errorHandler),
4647
security.NewOctopusInsecureFeedsCheck(o.client, config, o.errorHandler),
4748
security.NewOctopusInsecureSubscriptionsCheck(o.client, config, o.errorHandler),
49+
security.NewOctoLintSha1CertificatesCheck(o.client, config, o.errorHandler),
4850
organization.NewOctopusEnvironmentCountCheck(o.client, config, o.errorHandler),
4951
organization.NewOctopusDefaultProjectGroupCountCheck(o.client, config, o.errorHandler),
5052
organization.NewOctopusEmptyProjectCheck(o.client, config, o.errorHandler),
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package security
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"sort"
9+
"strings"
10+
11+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
12+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/machines"
13+
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks"
14+
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/client_wrapper"
15+
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/config"
16+
"go.uber.org/zap"
17+
)
18+
19+
const (
20+
OctoLintSha1Certificates = "OctoLintSha1Certificates"
21+
sha1Alg = "sha1RSA"
22+
)
23+
24+
// OctoLintSha1CertificatesCheck checks to see if any targets, workers or the server itself is using a sha1 certificate
25+
type OctoLintSha1CertificatesCheck struct {
26+
client *client.Client
27+
errorHandler checks.OctopusClientErrorHandler
28+
config *config.OctolintConfig
29+
}
30+
31+
type Sha1CertificateResult struct {
32+
Name string
33+
Type string // "Target", "Worker", or "Global"
34+
}
35+
36+
type ServerCertificate struct {
37+
ID string `json:"Id"`
38+
Name string `json:"Name"`
39+
Thumbprint string `json:"Thumbprint"`
40+
SignatureAlgorithm string `json:"SignatureAlgorithm"`
41+
Links map[string]string `json:"Links"`
42+
}
43+
44+
func NewOctoLintSha1CertificatesCheck(client *client.Client, config *config.OctolintConfig, errorHandler checks.OctopusClientErrorHandler) OctoLintSha1CertificatesCheck {
45+
return OctoLintSha1CertificatesCheck{config: config, client: client, errorHandler: errorHandler}
46+
}
47+
48+
func (o OctoLintSha1CertificatesCheck) Id() string {
49+
return OctoLintSha1Certificates
50+
}
51+
52+
// fetchServerCertificate gets the server certificate object and returns it.
53+
func fetchServerCertificate(url, apiKey, accessToken string) (*ServerCertificate, error) {
54+
requestURL := fmt.Sprintf("%s/api/configuration/certificates/certificate-global", url)
55+
56+
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
if apiKey != "" {
62+
req.Header.Set("X-Octopus-ApiKey", apiKey)
63+
} else if accessToken != "" {
64+
req.Header.Set("Authorization", "Bearer "+accessToken)
65+
}
66+
67+
res, err := http.DefaultClient.Do(req)
68+
if err != nil {
69+
return nil, err
70+
}
71+
defer res.Body.Close()
72+
73+
if res.StatusCode != http.StatusOK {
74+
return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
75+
}
76+
77+
var cert ServerCertificate
78+
if err := json.NewDecoder(res.Body).Decode(&cert); err != nil {
79+
return nil, err
80+
}
81+
82+
return &cert, nil
83+
}
84+
85+
// hasSha1Certificate checks if an endpoint has CertificateSignatureAlgorithm == sha1Alg
86+
func hasSha1Certificate(ep machines.IEndpoint) bool {
87+
if ep == nil {
88+
return false
89+
}
90+
91+
switch e := ep.(type) {
92+
case *machines.ListeningTentacleEndpoint:
93+
return e.CertificateSignatureAlgorithm == sha1Alg
94+
case *machines.PollingTentacleEndpoint:
95+
return e.CertificateSignatureAlgorithm == sha1Alg
96+
default:
97+
return false
98+
}
99+
}
100+
101+
// addSha1FromMachines is a top-level generic helper (function literals can't have type params).
102+
func addSha1FromMachines[T any](
103+
results *[]Sha1CertificateResult,
104+
items []T,
105+
getName func(T) string,
106+
getEndpoint func(T) machines.IEndpoint,
107+
typ string,
108+
) {
109+
for _, item := range items {
110+
if hasSha1Certificate(getEndpoint(item)) {
111+
*results = append(*results, Sha1CertificateResult{Name: getName(item), Type: typ})
112+
}
113+
}
114+
}
115+
116+
func (o OctoLintSha1CertificatesCheck) Execute(concurrency int) (checks.OctopusCheckResult, error) {
117+
if o.client == nil {
118+
return nil, errors.New("octoclient is nil")
119+
}
120+
121+
zap.L().Debug("Starting check " + o.Id())
122+
defer func() {
123+
zap.L().Debug("Ended check " + o.Id())
124+
}()
125+
126+
var results []Sha1CertificateResult
127+
128+
// Check server certificate
129+
cert, err := fetchServerCertificate(o.config.Url, o.config.ApiKey, o.config.AccessToken)
130+
if err != nil {
131+
return o.errorHandler.HandleError(o.Id(), checks.Security, err)
132+
}
133+
if cert != nil && cert.SignatureAlgorithm == sha1Alg {
134+
results = append(results, Sha1CertificateResult{Name: cert.Name, Type: "Global"})
135+
}
136+
137+
// Check deployment targets
138+
targets, err := client_wrapper.GetMachines(o.config.MaxSha1CertificatesMachines, o.client, o.client.GetSpaceID())
139+
if err != nil {
140+
return o.errorHandler.HandleError(o.Id(), checks.Security, err)
141+
}
142+
addSha1FromMachines(&results, targets,
143+
func(m *machines.DeploymentTarget) string { return m.Name },
144+
func(m *machines.DeploymentTarget) machines.IEndpoint { return m.Endpoint },
145+
"Target",
146+
)
147+
148+
// Check workers
149+
workers, err := client_wrapper.GetWorkers(o.config.MaxSha1CertificatesMachines, o.client, o.client.GetSpaceID())
150+
if err != nil {
151+
return o.errorHandler.HandleError(o.Id(), checks.Security, err)
152+
}
153+
addSha1FromMachines(&results, workers,
154+
func(w *machines.Worker) string { return w.Name },
155+
func(w *machines.Worker) machines.IEndpoint { return w.Endpoint },
156+
"Worker",
157+
)
158+
159+
// Provide results
160+
if len(results) > 0 {
161+
// Sort by Type then Name for stable output
162+
sort.Slice(results, func(i, j int) bool {
163+
if results[i].Type == results[j].Type {
164+
return results[i].Name < results[j].Name
165+
}
166+
return results[i].Type < results[j].Type
167+
})
168+
169+
lines := make([]string, len(results))
170+
for i, m := range results {
171+
lines[i] = fmt.Sprintf("%s: %s", m.Type, m.Name)
172+
}
173+
174+
return checks.NewOctopusCheckResultImpl(
175+
"The following resources use a SHA1 certificate:\n"+strings.Join(lines, "\n"),
176+
o.Id(),
177+
"",
178+
checks.Warning,
179+
checks.Security), nil
180+
}
181+
182+
return checks.NewOctopusCheckResultImpl(
183+
"There are no uses of SHA1 certificates in targets, workers or the main Server Certificate",
184+
o.Id(),
185+
"",
186+
checks.Ok,
187+
checks.Security), nil
188+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package security
2+
3+
import (
4+
"errors"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
9+
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks"
10+
"github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/config"
11+
"github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/octoclient"
12+
"github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/test"
13+
)
14+
15+
func TestSha1Certificates(t *testing.T) {
16+
testFramework := test.OctopusContainerTest{}
17+
18+
testFramework.ArrangeTest(t, func(t *testing.T, container *test.OctopusContainer, client *client.Client) error {
19+
// Act: Deploy Terraform scenario that sets up SHA1 certificates
20+
newSpaceId, err := testFramework.Act(
21+
t,
22+
container,
23+
filepath.Join("..", "..", "..", "test", "terraform"),
24+
"33-sha1certificates", // folder containing your Terraform scenario
25+
[]string{},
26+
)
27+
if err != nil {
28+
return err
29+
}
30+
31+
// Create a client for the new space
32+
newSpaceClient, err := octoclient.CreateClient(container.URI, newSpaceId, test.ApiKey)
33+
if err != nil {
34+
return err
35+
}
36+
37+
// Create the check
38+
check := NewOctoLintSha1CertificatesCheck(
39+
newSpaceClient,
40+
&config.OctolintConfig{
41+
Url: container.URI,
42+
ApiKey: test.ApiKey,
43+
MaxSha1CertificatesMachines: 100,
44+
},
45+
checks.OctopusClientPermissiveErrorHandler{},
46+
)
47+
48+
// Execute the check
49+
result, err := check.Execute(2)
50+
if err != nil {
51+
return err
52+
}
53+
54+
// Assert
55+
if result == nil || result.Severity() != checks.Warning {
56+
return errors.New("check should have failed")
57+
}
58+
59+
return nil
60+
})
61+
}

internal/client_wrapper/workers.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package client_wrapper
2+
3+
import (
4+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/machines"
5+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/newclient"
6+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/workers"
7+
)
8+
9+
func GetWorkers(limit int, client newclient.Client, spaceID string) ([]*machines.Worker, error) {
10+
if limit == 0 {
11+
return workers.GetAll(client, spaceID)
12+
}
13+
14+
result, err := workers.Get(client, spaceID, machines.WorkersQuery{
15+
Take: limit,
16+
})
17+
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
return result.Items, nil
23+
}

internal/config/args.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ type OctolintConfig struct {
8080
MaxInvalidNameTargets int
8181
MaxInsecureK8sTargets int
8282
MaxDeploymentTasks int
83+
MaxSha1CertificatesMachines int
8384
}
8485

8586
type StringSliceArgs []string

internal/defaults/defaultValues.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ const MaxTenantTagsTenants = 100
2424
const MaxProjectSpecificEnvironmentEnvironments = 100
2525
const MaxInvalidNameTargets = 100
2626
const MaxInsecureK8sTargets = 100
27+
const MaxSha1CertificatesMachines = 100
2728
const MaxDeploymentTasks = 100
2829
const MaxDefaultStepNameProjects = 100
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
terraform {
2+
required_providers {
3+
octopusdeploy = { source = "OctopusDeployLabs/octopusdeploy", version = "0.30.4" }
4+
}
5+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
resource "octopusdeploy_environment" "development_environment" {
2+
allow_dynamic_infrastructure = true
3+
description = "A development environment"
4+
name = "Development"
5+
use_guided_failure = false
6+
}
7+
8+
resource "octopusdeploy_environment" "test_environment" {
9+
allow_dynamic_infrastructure = true
10+
description = "A test environment"
11+
name = "Test"
12+
use_guided_failure = false
13+
}
14+
15+
resource "octopusdeploy_environment" "production_environment" {
16+
allow_dynamic_infrastructure = true
17+
description = "A production environment"
18+
name = "Production"
19+
use_guided_failure = false
20+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
provider "octopusdeploy" {
2+
address = "${var.octopus_server}"
3+
api_key = "${var.octopus_apikey}"
4+
space_id = "${var.octopus_space_id}"
5+
}

0 commit comments

Comments
 (0)