(
+
+ )}
+ />
+ );
+ }
+
+ return (
+
+ );
+};
+
const ActionButtons = ({ baseClass, actions }: IProps): JSX.Element => {
const primaryActions: IActionButtonProps[] = [];
const secondaryActions: IActionButtonProps[] = [];
@@ -55,56 +90,9 @@ const ActionButtons = ({ baseClass, actions }: IProps): JSX.Element => {
- {secondaryActions.map((action) => {
- if (!action.hideAction && action.buttonVariant !== "text-icon") {
- if (action.gitOpsModeCompatible) {
- return (
- (
-
- )}
- />
- );
- }
- return (
-
- );
- }
- if (action.gitOpsModeCompatible) {
- return (
- (
-
- )}
- />
- );
- }
- return (
-
- );
- })}
+ {secondaryActions.map(
+ (action) => !action.hideAction && renderSecondaryAction(action)
+ )}
Date: Mon, 14 Sep 2026 06:43:17 +0000
Subject: [PATCH 30/40] fix(adhoc-sweep-fixes): 61 review findings across 40
files
---
server/mdm/acme/internal/service/account_order.go | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/server/mdm/acme/internal/service/account_order.go b/server/mdm/acme/internal/service/account_order.go
index eb399cd137b..125a5f3542d 100644
--- a/server/mdm/acme/internal/service/account_order.go
+++ b/server/mdm/acme/internal/service/account_order.go
@@ -104,12 +104,16 @@ func (s *Service) createOrderResponse(
return nil, ctxerr.Wrap(ctx, err, "constructing finalize URL for account")
}
- var authzURL string
+ // NOTE: we only support a single authorization per order right now; if we add more we need to re-work this
+ var authzURLs []string
if len(authorizations) == 1 {
- authzURL, err = s.getACMEURLWithBaseURL(ctx, baseURL, enrollment.PathIdentifier, "authorizations", fmt.Sprint(authorizations[0].ID))
+ authzURL, err := s.getACMEURLWithBaseURL(ctx, baseURL, enrollment.PathIdentifier, "authorizations", fmt.Sprint(authorizations[0].ID))
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "constructing authorization URL for account")
}
+ authzURLs = []string{authzURL}
+ } else {
+ authzURLs = []string{}
}
var certURL string
@@ -125,7 +129,7 @@ func (s *Service) createOrderResponse(
Status: order.Status,
Expires: enrollment.NotValidAfter,
Identifiers: order.Identifiers,
- Authorizations: []string{authzURL},
+ Authorizations: authzURLs,
Finalize: finalizeURL,
Certificate: certURL,
Location: orderURL,
From e21b3a2ac154b57e5236bcaefa9d8685531f4d02 Mon Sep 17 00:00:00 2001
From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 06:43:19 +0000
Subject: [PATCH 31/40] fix(adhoc-sweep-fixes): 61 review findings across 40
files
---
server/service/global_policies_test.go | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/server/service/global_policies_test.go b/server/service/global_policies_test.go
index 06fe59fc8e1..595bacdbda1 100644
--- a/server/service/global_policies_test.go
+++ b/server/service/global_policies_test.go
@@ -159,6 +159,17 @@ func TestGlobalPoliciesAuth(t *testing.T) {
// by ID" endpoint refuses to return a team policy to a user who has no role
// on that team. This guards against the regression described in the
// "Cross-Team Policy Data Exposure" disclosure.
+//
+// This test asserts behavior at the service layer only (via the svc.authz
+// gate invoked inside GetPolicyByID). It does not by itself prove that the
+// production authorization check in server/service/global_policies.go
+// (GetPolicyByID) has not regressed on refactor: if a future change replaces
+// or bypasses the svc.authz.Authorize call for fleet.ActionRead against the
+// policy's *fleet.Team, these test cases would need to keep failing for that
+// regression to be caught. Reviewers modifying GetPolicyByID must confirm the
+// authorization check against the policy's TeamID (nil, 0, or a specific
+// team) is still performed before returning policy data, not merely that
+// these tests are green.
func TestGetPolicyByIDCrossTeamAuth(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
From 87cc2f13f54d79d45bf75a11a9fea8c4e8861ef0 Mon Sep 17 00:00:00 2001
From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 06:43:20 +0000
Subject: [PATCH 32/40] fix(adhoc-sweep-fixes): 61 review findings across 40
files
---
...095046_AddUniqconstraintSoftwareIDOnSoftwareCVE.go | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/server/datastore/mysql/migrations/tables/20220708095046_AddUniqconstraintSoftwareIDOnSoftwareCVE.go b/server/datastore/mysql/migrations/tables/20220708095046_AddUniqconstraintSoftwareIDOnSoftwareCVE.go
index b534b5e3a84..4629f8f1a75 100644
--- a/server/datastore/mysql/migrations/tables/20220708095046_AddUniqconstraintSoftwareIDOnSoftwareCVE.go
+++ b/server/datastore/mysql/migrations/tables/20220708095046_AddUniqconstraintSoftwareIDOnSoftwareCVE.go
@@ -96,15 +96,14 @@ func Up_20220708095046(tx *sql.Tx) error {
// the constraint and new duplicates get generated in between, we need to try to acquire the
// vulnerability lock. In case the lock can't be acquired a warning is issued and the migration
// will proceed without it.
+ locked := false
identifier, err := server.GenerateRandomText(64)
if err != nil {
logger.Warn.Println("Could not generate identifier for lock, might not be able to remove duplicates in a reliable way...")
} else {
- locked, err := acquireLock(tx, identifier)
+ locked, err = acquireLock(tx, identifier)
if !locked || err != nil {
logger.Warn.Println("Could not acquire lock, might not be able to remove duplicates in a reliable way...")
- } else {
- defer releaseLock(tx, identifier) //nolint:errcheck
}
}
@@ -116,8 +115,10 @@ func Up_20220708095046(tx *sql.Tx) error {
return err
}
- if err := releaseLock(tx, identifier); err != nil {
- return err
+ if locked {
+ if err := releaseLock(tx, identifier); err != nil {
+ return err
+ }
}
return nil
From d60f95c63d16d9254248228d6b5c575a001d0ad7 Mon Sep 17 00:00:00 2001
From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 06:43:21 +0000
Subject: [PATCH 33/40] fix(adhoc-sweep-fixes): 61 review findings across 40
files
---
server/mock/datastore.go | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/server/mock/datastore.go b/server/mock/datastore.go
index a49ee60022b..bcaf0efed6d 100644
--- a/server/mock/datastore.go
+++ b/server/mock/datastore.go
@@ -35,10 +35,15 @@ func (m *Store) GetCurrentTime(ctx context.Context) (time.Time, error) {
return time.Time{}, nil
}
-func (m *Store) Drop() error { return nil }
-func (m *Store) MigrateTables(ctx context.Context) error { return nil }
-func (m *Store) MigrateData(ctx context.Context) error { return nil }
-func (m *Store) MigrateOpenframe(ctx context.Context) error { return nil }
+// NOTE: Drop, MigrateTables, MigrateData, MigrateOpenframe, MigrationStatus and Name
+// are deliberately hand-written here rather than generated by mockimpl, since they
+// are simple no-op stubs used across tests. If fleet.Datastore's method set changes
+// for any of these methods, the compiler will fail to satisfy the
+// `var _ fleet.Datastore = (*Store)(nil)` assertion above, surfacing the drift.
+func (m *Store) Drop() error { return nil }
+func (m *Store) MigrateTables(ctx context.Context) error { return nil }
+func (m *Store) MigrateData(ctx context.Context) error { return nil }
+func (m *Store) MigrateOpenframe(ctx context.Context) error { return nil }
func (m *Store) MigrationStatus(ctx context.Context) (*fleet.MigrationStatus, error) {
return &fleet.MigrationStatus{}, nil
}
From f81155ea46628924f1580d87b567809ae213ad2e Mon Sep 17 00:00:00 2001
From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 06:43:22 +0000
Subject: [PATCH 34/40] fix(adhoc-sweep-fixes): 61 review findings across 40
files
---
tools/seed_data/queries/seed_queries.go | 20 ++++++++++++++------
1 file changed, 14 insertions(+), 6 deletions(-)
diff --git a/tools/seed_data/queries/seed_queries.go b/tools/seed_data/queries/seed_queries.go
index c7e1f00dd19..7f36630a901 100644
--- a/tools/seed_data/queries/seed_queries.go
+++ b/tools/seed_data/queries/seed_queries.go
@@ -4,6 +4,7 @@ import (
"database/sql"
"fmt"
"log"
+ "os"
"strings"
_ "github.com/go-sql-driver/mysql"
@@ -14,13 +15,20 @@ const (
totalRecords = 1000000
)
+func getEnvOrDefault(key, defaultValue string) string {
+ if value, ok := os.LookupEnv(key); ok {
+ return value
+ }
+ return defaultValue
+}
+
func main() {
- // MySQL connection details from your Docker Compose file
- user := "fleet"
- password := "insecure"
- host := "localhost" // Assuming you are running this script on the same host as Docker
- port := "3306"
- database := "fleet"
+ // MySQL connection details, overridable via environment variables for local dev
+ user := getEnvOrDefault("SEED_MYSQL_USER", "fleet")
+ password := getEnvOrDefault("SEED_MYSQL_PASSWORD", "insecure")
+ host := getEnvOrDefault("SEED_MYSQL_HOST", "localhost") // Assuming you are running this script on the same host as Docker
+ port := getEnvOrDefault("SEED_MYSQL_PORT", "3306")
+ database := getEnvOrDefault("SEED_MYSQL_DATABASE", "fleet")
// Construct the MySQL DSN (Data Source Name)
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, password, host, port, database)
From b34b25baf3e61e9d73cb4d9655150d12eafc5ad7 Mon Sep 17 00:00:00 2001
From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 06:43:24 +0000
Subject: [PATCH 35/40] fix(adhoc-sweep-fixes): 61 review findings across 40
files
---
.../entrance/update-password-and-login.js | 30 +++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/ee/vulnerability-dashboard/api/controllers/entrance/update-password-and-login.js b/ee/vulnerability-dashboard/api/controllers/entrance/update-password-and-login.js
index 51a75b8746d..ec371d449df 100644
--- a/ee/vulnerability-dashboard/api/controllers/entrance/update-password-and-login.js
+++ b/ee/vulnerability-dashboard/api/controllers/entrance/update-password-and-login.js
@@ -34,6 +34,11 @@ module.exports = {
invalidToken: {
description: 'The provided password token is invalid, expired, or has already been used.',
responseType: 'expired'
+ },
+
+ tooManyAttempts: {
+ description: 'Too many invalid password token attempts have been made from this requester recently.',
+ responseType: 'tooManyRequests'
}
},
@@ -45,14 +50,38 @@ module.exports = {
throw 'invalidToken';
}
+ // Rate limit / lockout repeated invalid token attempts from this requesting
+ // user agent, to make brute-forcing a valid reset token impractical.
+ var rateLimitKey = 'passwordResetAttempts::' + this.req.ip;
+ sails._passwordResetAttemptsByKey = sails._passwordResetAttemptsByKey || {};
+ var attemptRecord = sails._passwordResetAttemptsByKey[rateLimitKey];
+ var now = Date.now();
+ var attemptWindowMs = 15 * 60 * 1000; // 15 minutes
+ var maxAttempts = 10;
+
+ if (attemptRecord && (now - attemptRecord.firstAttemptAt) < attemptWindowMs && attemptRecord.count >= maxAttempts) {
+ throw 'tooManyAttempts';
+ }
+
// Look up the user with this reset token.
var userRecord = await User.findOne({ passwordResetToken: token });
// If no such user exists, or their token is expired, bail.
if (!userRecord || userRecord.passwordResetTokenExpiresAt <= Date.now()) {
+
+ // Track this invalid attempt for rate limiting purposes.
+ if (!attemptRecord || (now - attemptRecord.firstAttemptAt) >= attemptWindowMs) {
+ attemptRecord = { firstAttemptAt: now, count: 0 };
+ sails._passwordResetAttemptsByKey[rateLimitKey] = attemptRecord;
+ }
+ attemptRecord.count++;
+
throw 'invalidToken';
}
+ // On a successful token match, clear any tracked invalid attempts for this requester.
+ delete sails._passwordResetAttemptsByKey[rateLimitKey];
+
// Hash the new password.
var hashed = await sails.helpers.passwords.hashPassword(password);
@@ -78,3 +107,4 @@ module.exports = {
};
+
From 47f5a54884fcc4e0dbdb09c6da52bb5f818e62fd Mon Sep 17 00:00:00 2001
From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 06:43:25 +0000
Subject: [PATCH 36/40] fix(adhoc-sweep-fixes): 61 review findings across 40
files
---
frontend/services/entities/sessions.ts | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/frontend/services/entities/sessions.ts b/frontend/services/entities/sessions.ts
index db5796db867..cad4e29b90e 100644
--- a/frontend/services/entities/sessions.ts
+++ b/frontend/services/entities/sessions.ts
@@ -26,6 +26,16 @@ export interface ILoginResponse {
token_expires_at?: string;
}
+export class MfaRequiredError extends Error {
+ response: unknown;
+
+ constructor(rawResponse: unknown) {
+ super("MFA required");
+ this.name = "MfaRequiredError";
+ this.response = rawResponse;
+ }
+}
+
export default {
login: ({ email, password }: ILoginProps): Promise => {
const { LOGIN } = endpoints;
@@ -45,7 +55,7 @@ export default {
).then((rawResponse) => {
if (rawResponse.status === 202) {
// MFA; treat as an error and let the caller handle it
- throw rawResponse;
+ throw new MfaRequiredError(rawResponse);
}
const response = rawResponse.data;
const { user } = response;
From 27a221df7a630f4243810f668557f4ec3e077e25 Mon Sep 17 00:00:00 2001
From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 06:43:26 +0000
Subject: [PATCH 37/40] fix(adhoc-sweep-fixes): 61 review findings across 40
files
---
.../tables/20260401153000_AddACMEAndRenameSCEPDepotTables.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server/datastore/mysql/migrations/tables/20260401153000_AddACMEAndRenameSCEPDepotTables.go b/server/datastore/mysql/migrations/tables/20260401153000_AddACMEAndRenameSCEPDepotTables.go
index 34a1ae2b8d6..c72c0482a60 100644
--- a/server/datastore/mysql/migrations/tables/20260401153000_AddACMEAndRenameSCEPDepotTables.go
+++ b/server/datastore/mysql/migrations/tables/20260401153000_AddACMEAndRenameSCEPDepotTables.go
@@ -88,7 +88,7 @@ func Up_20260401153000(tx *sql.Tx) error {
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
- FOREIGN KEY (acme_account_id) REFERENCES acme_accounts(id) ON DELETE CASCADE ON UPDATE CASCADE,
+ FOREIGN KEY (acme_account_id) REFERENCES acme_accounts(id) ON DELETE RESTRICT ON UPDATE CASCADE,
UNIQUE KEY idx_issued_certificate_serial (issued_certificate_serial)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`)
From ef47d5d82d141e490d68d16d7ac15e5429345e2f Mon Sep 17 00:00:00 2001
From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 06:43:27 +0000
Subject: [PATCH 38/40] fix(adhoc-sweep-fixes): 61 review findings across 40
files
---
tools/mdm/apple/setupexperience/main.go | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/tools/mdm/apple/setupexperience/main.go b/tools/mdm/apple/setupexperience/main.go
index 300250cfbd7..07b44476373 100644
--- a/tools/mdm/apple/setupexperience/main.go
+++ b/tools/mdm/apple/setupexperience/main.go
@@ -24,6 +24,9 @@ import (
func main() {
mysqlAddr := flag.String("mysql", "localhost:3306", "mysql address")
+ flagDBUser := flag.String("mysql-user", "fleet", "mysql username")
+ flagDBPass := flag.String("mysql-pass", "insecure", "mysql password")
+ flagDBName := flag.String("mysql-db", "fleet", "mysql database name")
serverPrivateKey := flag.String("server-private-key", "", "fleet server's private key (to decrypt MDM assets)")
hostUUID := flag.String("host-uuid", "", "the host serial # to enqueue setup items for")
@@ -43,9 +46,9 @@ func main() {
mysqlConf := config.MysqlConfig{
Protocol: "tcp",
Address: *mysqlAddr,
- Database: "fleet",
- Username: "fleet",
- Password: "insecure",
+ Database: *flagDBName,
+ Username: *flagDBUser,
+ Password: *flagDBPass,
MaxOpenConns: 50,
MaxIdleConns: 50,
ConnMaxLifetime: 0,
From 8652817ef43b50292c1a1139c20ba0ccd8bfd2ef Mon Sep 17 00:00:00 2001
From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 06:43:28 +0000
Subject: [PATCH 39/40] fix(adhoc-sweep-fixes): 61 review findings across 40
files
---
.../controllers/entrance/send-password-recovery-email.js | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/website/api/controllers/entrance/send-password-recovery-email.js b/website/api/controllers/entrance/send-password-recovery-email.js
index 46ce374f55f..4f9413039fc 100644
--- a/website/api/controllers/entrance/send-password-recovery-email.js
+++ b/website/api/controllers/entrance/send-password-recovery-email.js
@@ -16,9 +16,9 @@ module.exports = {
required: true
},
- websiteUrl: {
+ company: {
type: 'string',
- description: 'Honeypot field. If filled, the submission is silently discarded.'
+ description: 'Optional field.'
}
},
@@ -33,9 +33,9 @@ module.exports = {
},
- fn: async function ({emailAddress, websiteUrl}) {
+ fn: async function ({emailAddress, company}) {
- if (websiteUrl) { return; }// Honeypot input provided — return a success response
+ if (company) { return; }// Honeypot input provided — return a success response
// Find the record for this user.
// (Even if no such user exists, pretend it worked to discourage sniffing.)
@@ -71,3 +71,4 @@ module.exports = {
};
+
From fc7942aafb1aef920cab603a853312b9e0c0a5c3 Mon Sep 17 00:00:00 2001
From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 06:43:29 +0000
Subject: [PATCH 40/40] fix(adhoc-sweep-fixes): 61 review findings across 40
files
---
tools/mdm/windows/bitlocker/core.go | 299 ++++++++++++++++------------
1 file changed, 167 insertions(+), 132 deletions(-)
diff --git a/tools/mdm/windows/bitlocker/core.go b/tools/mdm/windows/bitlocker/core.go
index 376947712ae..bc5e05293eb 100755
--- a/tools/mdm/windows/bitlocker/core.go
+++ b/tools/mdm/windows/bitlocker/core.go
@@ -1,132 +1,167 @@
-package main
-
-import (
- "flag"
- "fmt"
-)
-
-func BitlockerEncryptionNumericalPassword(encryptionPassword string) error {
-
- // Connect to the volume
- vol, err := Connect("c:")
- if err != nil {
- return fmt.Errorf("there was an error connecting to the volume - error: %v", err)
- }
- defer vol.Close()
-
- // Prepare for encryption
- if err := vol.Prepare(VolumeTypeDefault, EncryptionTypeSoftware); err != nil {
- return fmt.Errorf("there was an error preparing the volume for encryption - error: %v", err)
- }
-
- // Add a recovery protector
-
- if err := vol.ProtectWithNumericalPassword(encryptionPassword); err != nil {
- return fmt.Errorf("there was an error adding a recovery protector - error: %v", err)
- }
-
- // Protect with TPM
- if err := vol.ProtectWithTPM(nil); err != nil {
- return fmt.Errorf("there was an error protecting with TPM - error: %v", err)
- }
-
- // Start encryption
- if err := vol.Encrypt(XtsAES256, EncryptDataOnly); err != nil {
- return fmt.Errorf("there was an error starting encryption - error: %v", err)
- }
-
- return nil
-}
-
-func BitlockerDecryption() error {
-
- // Connect to the volume
- vol, err := Connect("c:")
- if err != nil {
- return fmt.Errorf("there was an error connecting to the volume - error: %v", err)
- }
- defer vol.Close()
-
- // Start decryption
- if err := vol.Decrypt(); err != nil {
- return fmt.Errorf("there was an error starting decryption - error: %v", err)
- }
-
- return nil
-}
-
-func GetBitlockerStatus() (*EncryptionStatus, error) {
-
- // Connect to the volume
- vol, err := Connect("c:")
- if err != nil {
- return nil, fmt.Errorf("there was an error connecting to the volume - error: %v", err)
- }
- defer vol.Close()
-
- // Get volume status
- status, err := vol.GetBitlockerStatus()
- if err != nil {
- return nil, fmt.Errorf("there was an error starting decryption - error: %v", err)
- }
-
- return status, nil
-}
-
-func main() {
-
- enableBitlocker := flag.Bool("encrypt", false, "encrypt the drive")
- disableBitlocker := flag.Bool("decrypt", false, "decrypt the drive")
- statusBitlocker := flag.Bool("status", true, "get drive status")
-
- flag.Parse()
-
- if *enableBitlocker {
- fmt.Println("About to attempt enabling bitlocker")
-
- //This needs to be generated with algorithm defined at
- //https://learn.microsoft.com/en-us/windows/win32/secprov/getkeyprotectornumericalpassword-win32-encryptablevolume
- newPassword := "527230-472395-606199-107525-536789-168927-479336-471856"
-
- err := BitlockerEncryptionNumericalPassword(newPassword)
- if err != nil {
- fmt.Printf("bitlocker encryption error - %v\n", err)
- return
- }
-
- fmt.Println("Bitlocker encryption started!")
-
- } else if *disableBitlocker {
- fmt.Println("About to attempt disabling bitlocker")
-
- err := BitlockerDecryption()
- if err != nil {
- fmt.Printf("bitlocker decryption error - %v\n", err)
- return
- }
-
- fmt.Println("Bitlocker decryption started!")
-
- } else if *statusBitlocker {
- fmt.Println("About to get encryption status bitlocker")
-
- status, err := GetBitlockerStatus()
- if err != nil {
- fmt.Printf("bitlocker decryption error - %v\n", err)
- return
- }
-
- fmt.Println("Protection status: ", status.ProtectionStatusDesc)
- fmt.Println("Conversion status: ", status.ConversionStatusDesc)
- fmt.Println("Encryption Flags: ", status.EncryptionFlags)
- fmt.Println("Wiping Status description: ", status.WipingStatusDesc)
- fmt.Println("Encryption percentage complete: ", status.EncryptionPercentage)
- fmt.Println("Wiping percentage complete: ", status.WipingPercentage)
-
- fmt.Println("Bitlocker encryption status gathered!")
-
- } else {
- fmt.Println("You must specify either -encrypt, -decrypt or -status")
- return
- }
-}
+package main
+
+import (
+ "crypto/rand"
+ "flag"
+ "fmt"
+)
+
+func BitlockerEncryptionNumericalPassword(encryptionPassword string) error {
+
+ // Connect to the volume
+ vol, err := Connect("c:")
+ if err != nil {
+ return fmt.Errorf("there was an error connecting to the volume: %w", err)
+ }
+ defer vol.Close()
+
+ // Prepare for encryption
+ if err := vol.Prepare(VolumeTypeDefault, EncryptionTypeSoftware); err != nil {
+ return fmt.Errorf("there was an error preparing the volume for encryption: %w", err)
+ }
+
+ // Add a recovery protector
+
+ if err := vol.ProtectWithNumericalPassword(encryptionPassword); err != nil {
+ return fmt.Errorf("there was an error adding a recovery protector: %w", err)
+ }
+
+ // Protect with TPM
+ if err := vol.ProtectWithTPM(nil); err != nil {
+ return fmt.Errorf("there was an error protecting with TPM: %w", err)
+ }
+
+ // Start encryption
+ if err := vol.Encrypt(XtsAES256, EncryptDataOnly); err != nil {
+ return fmt.Errorf("there was an error starting encryption: %w", err)
+ }
+
+ return nil
+}
+
+func BitlockerDecryption() error {
+
+ // Connect to the volume
+ vol, err := Connect("c:")
+ if err != nil {
+ return fmt.Errorf("there was an error connecting to the volume: %w", err)
+ }
+ defer vol.Close()
+
+ // Start decryption
+ if err := vol.Decrypt(); err != nil {
+ return fmt.Errorf("there was an error starting decryption: %w", err)
+ }
+
+ return nil
+}
+
+func GetBitlockerStatus() (*EncryptionStatus, error) {
+
+ // Connect to the volume
+ vol, err := Connect("c:")
+ if err != nil {
+ return nil, fmt.Errorf("there was an error connecting to the volume: %w", err)
+ }
+ defer vol.Close()
+
+ // Get volume status
+ status, err := vol.GetBitlockerStatus()
+ if err != nil {
+ return nil, fmt.Errorf("there was an error getting bitlocker status: %w", err)
+ }
+
+ return status, nil
+}
+
+// generateNumericalRecoveryPassword generates a random 48-digit BitLocker
+// numerical recovery password formatted as 8 groups of 6 digits, per
+// https://learn.microsoft.com/en-us/windows/win32/secprov/getkeyprotectornumericalpassword-win32-encryptablevolume
+func generateNumericalRecoveryPassword() (string, error) {
+ const groups = 8
+ password := ""
+ for i := 0; i < groups; i++ {
+ if i > 0 {
+ password += "-"
+ }
+
+ max := int64(1000000) // 6 digits, 0-999999
+ b := make([]byte, 8)
+ if _, err := rand.Read(b); err != nil {
+ return "", fmt.Errorf("there was an error generating a random recovery password: %w", err)
+ }
+
+ var n int64
+ for _, v := range b {
+ n = (n << 8) | int64(v)
+ }
+ if n < 0 {
+ n = -n
+ }
+ n = n % max
+
+ password += fmt.Sprintf("%06d", n)
+ }
+
+ return password, nil
+}
+
+func main() {
+
+ enableBitlocker := flag.Bool("encrypt", false, "encrypt the drive")
+ disableBitlocker := flag.Bool("decrypt", false, "decrypt the drive")
+ statusBitlocker := flag.Bool("status", true, "get drive status")
+
+ flag.Parse()
+
+ if *enableBitlocker {
+ fmt.Println("About to attempt enabling bitlocker")
+
+ newPassword, err := generateNumericalRecoveryPassword()
+ if err != nil {
+ fmt.Printf("bitlocker encryption error - %v\n", err)
+ return
+ }
+
+ err = BitlockerEncryptionNumericalPassword(newPassword)
+ if err != nil {
+ fmt.Printf("bitlocker encryption error - %v\n", err)
+ return
+ }
+
+ fmt.Println("Bitlocker encryption started!")
+
+ } else if *disableBitlocker {
+ fmt.Println("About to attempt disabling bitlocker")
+
+ err := BitlockerDecryption()
+ if err != nil {
+ fmt.Printf("bitlocker decryption error - %v\n", err)
+ return
+ }
+
+ fmt.Println("Bitlocker decryption started!")
+
+ } else if *statusBitlocker {
+ fmt.Println("About to get encryption status bitlocker")
+
+ status, err := GetBitlockerStatus()
+ if err != nil {
+ fmt.Printf("bitlocker decryption error - %v\n", err)
+ return
+ }
+
+ fmt.Println("Protection status: ", status.ProtectionStatusDesc)
+ fmt.Println("Conversion status: ", status.ConversionStatusDesc)
+ fmt.Println("Encryption Flags: ", status.EncryptionFlags)
+ fmt.Println("Wiping Status description: ", status.WipingStatusDesc)
+ fmt.Println("Encryption percentage complete: ", status.EncryptionPercentage)
+ fmt.Println("Wiping percentage complete: ", status.WipingPercentage)
+
+ fmt.Println("Bitlocker encryption status gathered!")
+
+ } else {
+ fmt.Println("You must specify either -encrypt, -decrypt or -status")
+ return
+ }
+}