Skip to content

Commit 5e45665

Browse files
Merge pull request #8 from isaquepinheiro/fix/review-round3
fix: close every finding from the independent review
2 parents 938fcd6 + ac36d53 commit 5e45665

8 files changed

Lines changed: 263 additions & 319 deletions

File tree

README.md

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
[![Go Report Card][goReportBadge]][goReportLink]
66
[![GitHub release (latest by date)][latestReleaseBadge]](https://github.com/HashLoad/boss/releases/latest)
7-
[![CRA Compliance][craBadge]][securityPolicyLink]
87
[![SBOM Badge][sbomBadge]](https://www.pubpascal.dev)
98
[![GitHub Release Date][releaseDateBadge]](https://github.com/HashLoad/boss/releases)
109
[![GitHub repo size][repoSizeBadge]](https://github.com/HashLoad/boss/archive/refs/heads/main.zip)
@@ -130,28 +129,17 @@ These commands add modern project creation, compiling, and script running capabi
130129
#### > new
131130
Generate a fully structured Delphi or Lazarus project template (skeleton) in the current directory:
132131
```sh
133-
boss new delphi
134-
boss new lazarus
132+
boss new my_project
133+
boss new my_project --ide lazarus
134+
boss new my_package --type pkg --ide lazarus
135135
```
136136

137137
#### > pkg
138-
Perform Delphi package operations including packaging, signing, and verification.
138+
Perform Delphi package manifest operations.
139139
* **`pkg spec`**: Scaffolds a starter `pubpascal.json` manifest file for the package:
140140
```sh
141141
boss pkg spec --id my-package --pkgversion 1.0.0
142142
```
143-
* **`pkg pack`**: Build a redistributable package bundle (`.dpkg`):
144-
```sh
145-
boss pkg pack --spec pubpascal.json --output ./dist
146-
```
147-
* **`pkg sign`**: Statically sign a package bundle using a PFX certificate:
148-
```sh
149-
boss pkg sign --package mypkg.dpkg --pfx cert.pfx --pfx-password-env CERT_PASSWORD
150-
```
151-
* **`pkg verify`**: Verify a package bundle's signature and integrity:
152-
```sh
153-
boss pkg verify --package mypkg.dpkg
154-
```
155143

156144
#### > run
157145
Execute a custom shell script defined in the `scripts` section of your `boss.json` file:
@@ -179,9 +167,6 @@ Authenticate your local environment with the PubPascal portal using a Personal A
179167
```sh
180168
# Authenticate using a personal access token
181169
boss login --token <pat>
182-
183-
# Or start interactive login (prompts for the token)
184-
boss login portal
185170
```
186171
#### > contribute
187172
Contribute to a third-party package by automating repository forking and Pull Request creation.
@@ -196,7 +181,7 @@ Contribute to a third-party package by automating repository forking and Pull Re
196181

197182
#### > workspace
198183
Manage multi-repository PubPascal workspaces locally.
199-
* **`workspace clone`**: Clones a workspace and all its member repositories, setting up writable forks:
184+
* **`workspace clone`**: Clones a workspace and all its member repositories, checking out the reference each one is pinned to:
200185
```sh
201186
boss workspace clone <workspace-id>
202187
boss workspace clone <workspace-id> --codename my-branch
@@ -205,11 +190,11 @@ Manage multi-repository PubPascal workspaces locally.
205190
```sh
206191
boss workspace status
207192
```
208-
* **`workspace update`**: Fast-forward all repositories to their pinned reference branch/commit:
193+
* **`workspace update`**: Fast-forward each repository on its current branch:
209194
```sh
210195
boss workspace update
211196
```
212-
* **`workspace push`**: Push committed changes across all writable repositories in the workspace:
197+
* **`workspace push`**: Push committed changes for each repository that has an upstream branch:
213198
```sh
214199
boss workspace push
215200
```
@@ -222,7 +207,7 @@ These native commands help you achieve 100% Cyber Resilience Act (CRA) complianc
222207

223208
#### > cra
224209
Check your project's CRA compliance status or initialize required files automatically.
225-
* **`cra` (Diagnose)**: Scan the local project for required CRA signals (Security Policy, SBOM):
210+
* **`cra` (Diagnose)**: Scan the local project for required CRA signals (Security Policy, SBOM). It exits with status `1` when a signal is missing, so it can be used as a CI gate:
226211
```sh
227212
boss cra
228213
```
@@ -235,7 +220,7 @@ Check your project's CRA compliance status or initialize required files automati
235220
#### > sbom
236221
Generate a standard CycloneDX or SPDX Software Bill of Materials (SBOM) for your Delphi project:
237222
```sh
238-
# Generate CycloneDX SBOM (outputs to ./sbom/sbom.cdx.json)
223+
# Generate CycloneDX SBOM (outputs to ./sbom/<ProjectName>.cdx.json)
239224
boss sbom
240225

241226
# Specify custom project file and output path
@@ -245,19 +230,6 @@ boss sbom --project ./src/MyProj.dproj --output ./custom-sbom-folder
245230
boss sbom --format spdx
246231
```
247232

248-
#### > scan
249-
Scan a generated SBOM against the OSV.dev database for known vulnerabilities:
250-
```sh
251-
boss scan
252-
boss scan --sbom ./sbom/sbom.cdx.json
253-
```
254-
255-
#### > publish-sbom
256-
Upload a generated SBOM to the PubPascal portal for remote compliance badge rendering:
257-
```sh
258-
boss publish-sbom --slug my-pkg-slug --pkgversion 1.0.0 --file ./sbom/sbom.cdx.json
259-
```
260-
261233
---
262234

263235
### 5. Additional Commands
@@ -584,6 +556,4 @@ boss init -q
584556
[telegramBadge]: https://img.shields.io/badge/telegram-join%20channel-7289DA?style=flat-square
585557
[telegramLink]: https://t.me/hashload
586558
[repoStarsBadge]: https://img.shields.io/github/stars/hashload/boss?style=social
587-
[craBadge]: https://img.shields.io/badge/CRA-100%25%20compliant-brightgreen
588-
[securityPolicyLink]: SECURITY.md
589559
[sbomBadge]: https://img.shields.io/badge/SBOM-compliant-brightgreen

SECURITY.md

Lines changed: 0 additions & 59 deletions
This file was deleted.

internal/adapters/primary/cli/cmd_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,7 @@ func TestPubPascalCommands(t *testing.T) {
206206
if findCommand(root, "sbom") == nil {
207207
t.Error("Root command 'sbom' not found")
208208
}
209-
if findCommand(root, "publish-sbom") == nil {
210-
t.Error("Root command 'publish-sbom' not found")
211-
}
212209

213210
// Check pkg subcommands
214-
assertSubcommands(t, pkgCmd, "Pkg", []string{"spec", "pack"})
211+
assertSubcommands(t, pkgCmd, "Pkg", []string{"spec"})
215212
}

internal/adapters/primary/cli/contribute.go

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func handleForkSetupFlow(ctx context.Context, packageSlug string, pkgDir string,
8585
msg.Info("🍴 Requesting Fork from portal for %s...", packageSlug)
8686

8787
// Call Portal API to fork
88-
url := fmt.Sprintf("%s/api/packages/contribute/fork", config.PortalBaseURL)
88+
url := fmt.Sprintf("%s/api/packages/contribute/fork", strings.TrimSuffix(config.PortalBaseURL, "/"))
8989
requestBody, _ := json.Marshal(map[string]string{
9090
"packageSlug": packageSlug,
9191
})
@@ -104,11 +104,12 @@ func handleForkSetupFlow(ctx context.Context, packageSlug string, pkgDir string,
104104
}
105105
defer func() { _ = resp.Body.Close() }()
106106

107-
bodyBytes, _ := io.ReadAll(resp.Body)
107+
bodyBytes, err := readPortalBody(resp)
108+
if err != nil {
109+
msg.Die("❌ Failed to read portal response: %s", err)
110+
}
108111
if resp.StatusCode != http.StatusOK {
109-
var errRes map[string]string
110-
_ = json.Unmarshal(bodyBytes, &errRes)
111-
msg.Die("❌ Fork failed: %s", errRes["error"])
112+
msg.Die("❌ Fork failed (HTTP %d): %s", resp.StatusCode, portalErrorMessage(bodyBytes))
112113
}
113114

114115
var res struct {
@@ -226,7 +227,7 @@ func submitPullRequest(
226227
body string,
227228
) {
228229
// Call Portal API to create PR
229-
url := fmt.Sprintf("%s/api/packages/contribute/pr", config.PortalBaseURL)
230+
url := fmt.Sprintf("%s/api/packages/contribute/pr", strings.TrimSuffix(config.PortalBaseURL, "/"))
230231
requestBody, _ := json.Marshal(map[string]string{
231232
"packageSlug": packageSlug,
232233
"branch": branch,
@@ -248,11 +249,13 @@ func submitPullRequest(
248249
}
249250
defer func() { _ = resp.Body.Close() }()
250251

251-
bodyBytes, _ := io.ReadAll(resp.Body)
252+
bodyBytes, err := readPortalBody(resp)
253+
if err != nil {
254+
msg.Die("❌ Failed to read portal response: %s", err)
255+
}
252256
if resp.StatusCode != http.StatusOK {
253-
var errRes map[string]string
254-
_ = json.Unmarshal(bodyBytes, &errRes)
255-
msg.Die("❌ Pull Request creation failed: %s", errRes["error"])
257+
msg.Die("❌ Pull Request creation failed (HTTP %d): %s",
258+
resp.StatusCode, portalErrorMessage(bodyBytes))
256259
}
257260

258261
var res struct {
@@ -269,6 +272,44 @@ func submitPullRequest(
269272
msg.Info("🔗 Access your PR here: %s", res.PrURL)
270273
}
271274

275+
// maxPortalResponseBytes caps how much of a portal response is read into
276+
// memory. The payloads this command consumes are a handful of fields; anything
277+
// larger is a misbehaving or hostile endpoint, and io.ReadAll would happily
278+
// keep allocating for it.
279+
const maxPortalResponseBytes = 1 << 20 // 1 MiB
280+
281+
// readPortalBody reads a bounded portal response body.
282+
func readPortalBody(resp *http.Response) ([]byte, error) {
283+
return io.ReadAll(io.LimitReader(resp.Body, maxPortalResponseBytes))
284+
}
285+
286+
// portalErrorMessage extracts the message of a portal error response.
287+
//
288+
// Decoding into map[string]string used to fail as a whole as soon as any value
289+
// of the object was not a string -- a status code, a details object -- and the
290+
// user was shown an empty reason. Only the field that matters is decoded now,
291+
// and an unexpected payload falls back to the raw body instead of to nothing.
292+
func portalErrorMessage(body []byte) string {
293+
var errRes struct {
294+
Error string `json:"error"`
295+
Message string `json:"message"`
296+
}
297+
if err := json.Unmarshal(body, &errRes); err == nil {
298+
if errRes.Error != "" {
299+
return errRes.Error
300+
}
301+
if errRes.Message != "" {
302+
return errRes.Message
303+
}
304+
}
305+
306+
if raw := strings.TrimSpace(string(body)); raw != "" {
307+
return raw
308+
}
309+
310+
return "the portal returned no details"
311+
}
312+
272313
// Helper to run git commands.
273314
func runGitCmd(ctx context.Context, dir string, args ...string) (string, error) {
274315
// #nosec G204 -- fixed git binary; arguments are built by this CLI

internal/adapters/primary/cli/cra.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"os"
8+
"path/filepath"
89
"strings"
910

1011
"github.com/hashload/boss/pkg/msg"
@@ -27,7 +28,8 @@ func craCmdRegister(root *cobra.Command) {
2728
Use: cmdNameCRA,
2829
Short: "Cyber Resilience Act (CRA) compliance checker and assistant",
2930
Long: `Diagnose and automate Cyber Resilience Act (CRA) compliance for your Delphi project.
30-
Run without arguments to perform a local compliance check, or use 'cra init' to generate required files.`,
31+
Run without arguments to perform a local compliance check, or use 'cra init' to generate required files.
32+
The check exits with status 1 when a required signal is missing, so it can gate a CI job.`,
3133
Run: func(_ *cobra.Command, _ []string) {
3234
runCraCheck()
3335
},
@@ -70,7 +72,13 @@ func runCraCheck() {
7072
}
7173

7274
hasSbom := false
73-
sbomCandidates := []string{sbomFileName, "sbom.spdx.json", "bom.json", "sbom/" + sbomFileName}
75+
// boss sbom writes sbom/<ProjectName>.cdx.json, so a glob is needed on top
76+
// of the fixed names: matching only "sbom/sbom.cdx.json" meant the check
77+
// never saw the file the generator had just produced.
78+
sbomCandidates := []string{sbomFileName, "sbom.spdx.json", "bom.json"}
79+
if globbed, err := filepath.Glob(filepath.Join(sbomBaseName, "*.cdx.json")); err == nil {
80+
sbomCandidates = append(sbomCandidates, globbed...)
81+
}
7482
for _, c := range sbomCandidates {
7583
if _, err := os.Stat(c); err == nil {
7684
hasSbom = true
@@ -91,11 +99,18 @@ func runCraCheck() {
9199
if hasSecurity && hasSbom {
92100
msg.Info("\n🎉 Your local project is 100%% CRA compliant! Commit and push these files " +
93101
"to GitHub to get the Gold badge in the portal.")
94-
} else {
95-
msg.Info("\n💡 Tips to get 100%% CRA badge:")
96-
msg.Info("1. Run 'boss cra init' to let Boss generate the SECURITY.md and SBOM automatically.")
97-
msg.Info("2. Commit and push the files to your repository.")
102+
103+
return
98104
}
105+
106+
msg.Info("\n💡 Tips to get 100%% CRA badge:")
107+
msg.Info("1. Run 'boss cra init' to let Boss generate the SECURITY.md and SBOM automatically.")
108+
msg.Info("2. Commit and push the files to your repository.")
109+
110+
// A checker that always exits 0 cannot gate anything: a CI job would
111+
// report success on a project that has neither a security policy nor an
112+
// SBOM. Non-compliant is a failure, so the exit status has to say so.
113+
os.Exit(1)
99114
}
100115

101116
// runCraInit runs the interactive wizard to generate compliance files.

internal/adapters/primary/cli/login.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func loginCmdRegister(root *cobra.Command) {
3131
Aliases: []string{"adduser", "add-user"},
3232
Run: func(_ *cobra.Command, args []string) {
3333
if portalToken != "" {
34-
runPortalLogin(portalToken, args)
34+
runPortalLogin(portalToken)
3535
return
3636
}
3737
login(removeLogin, useSSH, privateKey, userName, password, args)

0 commit comments

Comments
 (0)