Skip to content

Commit 4031219

Browse files
abhisekSahilb315
andauthored
fix: Show error messages on fatal failures #32 (#34)
* fix: Show error messages on fatal failures #32 * test: Add E2E test * test: fix E2E scripts * test: fix E2E scripts * fix: Race condition in concurrent analyzer * fix: update formatting to ensure docs URL is clickable Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> * fix: E2E test --------- Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> Co-authored-by: Sahil Bansal <bansalsahil315@gmail.com>
1 parent b85f77c commit 4031219

12 files changed

Lines changed: 215 additions & 49 deletions

File tree

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,37 @@ jobs:
3636
with:
3737
token: ${{ secrets.CODECOV_TOKEN }}
3838

39+
e2e-test:
40+
runs-on: ubuntu-latest
41+
timeout-minutes: 15
42+
steps:
43+
- name: Checkout Source
44+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
45+
46+
- name: Setup Go
47+
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
48+
with:
49+
go-version: 1.24
50+
check-latest: true
51+
52+
- name: Setup Node
53+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
54+
with:
55+
node-version: 20
56+
check-latest: true
57+
58+
- name: Setup PNPM
59+
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4
60+
with:
61+
version: 10
62+
63+
- name: Build Binary
64+
run: make
65+
66+
- name: Run E2E Tests
67+
run: chmod +x test/e2e.sh && ./test/e2e.sh
68+
shell: bash
69+
3970
goreleaser-test:
4071
runs-on: ubuntu-latest
4172
timeout-minutes: 15

analyzer/malysis_active_scan.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ var _ Analyzer = &malysisActiveScanAnalyzer{}
3939

4040
func NewMalysisActiveScanAnalyzer(config MalysisActiveScanAnalyzerConfig) (*malysisActiveScanAnalyzer, error) {
4141
if config.TenantId == "" || config.ApiKey == "" {
42-
return nil, fmt.Errorf("active scanning requires SafeDep Cloud authentication credentials")
42+
return nil, fmt.Errorf("active scanning requires SafeDep Cloud credentials. See: https://docs.safedep.io/cloud/malware-analysis")
4343
}
4444

4545
headers := http.Header{}

cmd/npm/npm.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package npm
22

33
import (
4+
"context"
45
_ "embed"
56

67
"github.com/safedep/dry/log"
7-
"github.com/safedep/pmg/config"
8+
"github.com/safedep/pmg/internal/flows"
89
"github.com/safedep/pmg/internal/ui"
10+
"github.com/safedep/pmg/packagemanager"
911
"github.com/spf13/cobra"
1012
)
1113

@@ -15,12 +17,7 @@ func NewNpmCommand() *cobra.Command {
1517
Short: "Guard npm package manager",
1618
DisableFlagParsing: true,
1719
RunE: func(cmd *cobra.Command, args []string) error {
18-
config, err := config.FromContext(cmd.Context())
19-
if err != nil {
20-
ui.Fatalf("Failed to get config: %s", err)
21-
}
22-
23-
err = executeNpmFlow(cmd.Context(), config, args)
20+
err := executeNpmFlow(cmd.Context(), args)
2421
if err != nil {
2522
log.Errorf("Failed to execute npm flow: %s", err)
2623
}
@@ -29,3 +26,12 @@ func NewNpmCommand() *cobra.Command {
2926
},
3027
}
3128
}
29+
30+
func executeNpmFlow(ctx context.Context, args []string) error {
31+
packageManager, err := packagemanager.NewNpmPackageManager(packagemanager.DefaultNpmPackageManagerConfig())
32+
if err != nil {
33+
ui.Fatalf("Failed to create npm package manager proxy: %s", err)
34+
}
35+
36+
return flows.Common(packageManager).Run(ctx, args)
37+
}

cmd/npm/pnpm.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package npm
22

33
import (
4+
"context"
45
_ "embed"
56

67
"github.com/safedep/dry/log"
7-
"github.com/safedep/pmg/config"
8+
"github.com/safedep/pmg/internal/flows"
89
"github.com/safedep/pmg/internal/ui"
10+
"github.com/safedep/pmg/packagemanager"
911
"github.com/spf13/cobra"
1012
)
1113

@@ -15,12 +17,7 @@ func NewPnpmCommand() *cobra.Command {
1517
Short: "Guard pnpm package manager",
1618
DisableFlagParsing: true,
1719
RunE: func(cmd *cobra.Command, args []string) error {
18-
config, err := config.FromContext(cmd.Context())
19-
if err != nil {
20-
ui.Fatalf("Failed to get config: %s", err)
21-
}
22-
23-
err = executePnpmFlow(cmd.Context(), config, args)
20+
err := executePnpmFlow(cmd.Context(), args)
2421
if err != nil {
2522
log.Errorf("Failed to execute pnpm flow: %s", err)
2623
}
@@ -29,3 +26,12 @@ func NewPnpmCommand() *cobra.Command {
2926
},
3027
}
3128
}
29+
30+
func executePnpmFlow(ctx context.Context, args []string) error {
31+
packageManager, err := packagemanager.NewNpmPackageManager(packagemanager.DefaultPnpmPackageManagerConfig())
32+
if err != nil {
33+
ui.Fatalf("Failed to create pnpm package manager proxy: %s", err)
34+
}
35+
36+
return flows.Common(packageManager).Run(ctx, args)
37+
}

docs/development.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Development
2+
3+
- [User Interface](./ui.md)

docs/ui.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# User Interface
2+
3+
PMG is an interactive tool. We support multiple interactivity modes such as `silent`, `verbose` etc. to meet different developer experience needs. As such, we need to standardize the UI, UX and interactive messaging guidance for developers.
4+
5+
## Messaging
6+
7+
Two types of messages to users are supported:
8+
9+
1. UI messages
10+
2. Logs
11+
12+
### UI messages
13+
14+
UI messages are displayed in the user interface, currently in the terminal. Following types of messages are supported:
15+
16+
1. **Status updates** - Meant for showing the stage or status of the workflow.
17+
2. **Error messages** - Meant for showing fatal error messages
18+
19+
| Type | Mode | Show? |
20+
| ------ | ------- | ----- |
21+
| Status | Silent | No |
22+
| Status | Verbose | Yes |
23+
| Error | Silent | Yes |
24+
| Error | Verbose | Yes |
25+
26+
### Logs
27+
28+
Logs are by default for inspection and debugging purposes. They are not shown by default but can be configured through verbosity levels or logging to files. Consider logs as something meant for use only when there is an unexpected behavior.
29+

guard/guard.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ func (g *packageManagerGuard) continueExecution(ctx context.Context, pc *package
172172
cmd.Stdout = os.Stdout
173173
cmd.Stderr = os.Stderr
174174

175+
// We will fail based on executed command's exit code. This is important
176+
// because other tools (scripts, CI etc.) may depend on this exit code.
175177
return cmd.Run()
176178
}
177179

@@ -204,13 +206,19 @@ func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
204206
}()
205207
}
206208

209+
// Queue all packages for analysis
207210
for _, pkg := range packages {
208211
jobs <- pkg
209212
}
210213
close(jobs)
211214

212215
analysisResults := []*analyzer.PackageVersionAnalysisResult{}
216+
217+
// We must wait for the results go routine to collect all results
218+
rwg := sync.WaitGroup{}
219+
rwg.Add(1)
213220
go func() {
221+
defer rwg.Done()
214222
for result := range results {
215223
analysisResults = append(analysisResults, result)
216224
}
@@ -219,8 +227,10 @@ func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
219227
waiter := make(chan struct{})
220228
go func() {
221229
wg.Wait()
222-
close(waiter)
223230
close(results)
231+
232+
rwg.Wait()
233+
close(waiter)
224234
}()
225235

226236
select {

guard/guard_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package guard
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
8+
"github.com/safedep/pmg/analyzer"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestGuardConcurrentlyAnalyzePackagesMalwareQueryService(t *testing.T) {
13+
mq, err := analyzer.NewMalysisQueryAnalyzer(analyzer.MalysisQueryAnalyzerConfig{})
14+
if err != nil {
15+
t.Fatalf("failed to create mq: %v", err)
16+
}
17+
18+
pg, err := NewPackageManagerGuard(DefaultPackageManagerGuardConfig(), nil, nil,
19+
[]analyzer.PackageVersionAnalyzer{mq}, PackageManagerGuardInteraction{})
20+
if err != nil {
21+
t.Fatalf("failed to create pg: %v", err)
22+
}
23+
24+
t.Run("should resolve a single known malicious package version", func(t *testing.T) {
25+
r, err := pg.concurrentAnalyzePackages(context.Background(), []*packagev1.PackageVersion{
26+
{
27+
Package: &packagev1.Package{
28+
Name: "nyc-config",
29+
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
30+
},
31+
Version: "10.0.0",
32+
},
33+
})
34+
if err != nil {
35+
t.Fatalf("failed to analyze packages: %v", err)
36+
}
37+
38+
assert.Equal(t, 1, len(r))
39+
assert.Equal(t, "nyc-config", r[0].PackageVersion.GetPackage().GetName())
40+
assert.Equal(t, "10.0.0", r[0].PackageVersion.GetVersion())
41+
assert.Equal(t, packagev1.Ecosystem_ECOSYSTEM_NPM, r[0].PackageVersion.GetPackage().GetEcosystem())
42+
assert.NotEmpty(t, r[0].ReferenceURL)
43+
assert.NotEmpty(t, r[0].Summary)
44+
assert.NotNil(t, r[0].Data)
45+
assert.Equal(t, analyzer.ActionBlock, r[0].Action)
46+
})
47+
}
Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
package npm
1+
package flows
22

33
import (
44
"context"
5-
"fmt"
65

76
"github.com/safedep/pmg/analyzer"
87
"github.com/safedep/pmg/config"
@@ -11,30 +10,48 @@ import (
1110
"github.com/safedep/pmg/packagemanager"
1211
)
1312

14-
func executeCommonFlow(ctx context.Context, config config.Config, pm packagemanager.PackageManager, args []string) error {
13+
type commonFlow struct {
14+
pm packagemanager.PackageManager
15+
}
16+
17+
// Creates a common flow of execution for all package managers. This should work for most
18+
// of the cases unless a package manager has its own unique requirements. Configuration
19+
// should be passed through the context (Global Config)
20+
func Common(pm packagemanager.PackageManager) *commonFlow {
21+
return &commonFlow{
22+
pm: pm,
23+
}
24+
}
25+
26+
func (f *commonFlow) Run(ctx context.Context, args []string) error {
27+
config, err := config.FromContext(ctx)
28+
if err != nil {
29+
ui.Fatalf("Failed to get config: %s", err)
30+
}
31+
1532
packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig()
1633
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive
1734
packageResolverConfig.TransitiveDepth = config.TransitiveDepth
1835
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies
1936

2037
packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig)
2138
if err != nil {
22-
return fmt.Errorf("failed to create npm dependency resolver: %w", err)
39+
ui.Fatalf("Failed to create dependency resolver: %s", err)
2340
}
2441

2542
var analyzers []analyzer.PackageVersionAnalyzer
2643

2744
if config.Paranoid {
2845
malysisActiveScanAnalyzer, err := analyzer.NewMalysisActiveScanAnalyzer(analyzer.DefaultMalysisActiveScanAnalyzerConfig())
2946
if err != nil {
30-
return fmt.Errorf("failed to create malysis active scan analyzer: %w", err)
47+
ui.Fatalf("Failed to create malware analyzer: %s", err)
3148
}
3249

3350
analyzers = append(analyzers, malysisActiveScanAnalyzer)
3451
} else {
3552
malysisQueryAnalyzer, err := analyzer.NewMalysisQueryAnalyzer(analyzer.MalysisQueryAnalyzerConfig{})
3653
if err != nil {
37-
return fmt.Errorf("failed to create malysis query analyzer: %w", err)
54+
ui.Fatalf("Failed to create malware analyzer: %s", err)
3855
}
3956

4057
analyzers = append(analyzers, malysisQueryAnalyzer)
@@ -50,28 +67,15 @@ func executeCommonFlow(ctx context.Context, config config.Config, pm packagemana
5067
guardConfig := guard.DefaultPackageManagerGuardConfig()
5168
guardConfig.DryRun = config.DryRun
5269

53-
proxy, err := guard.NewPackageManagerGuard(guardConfig, pm, packageResolver, analyzers, interaction)
70+
proxy, err := guard.NewPackageManagerGuard(guardConfig, f.pm, packageResolver, analyzers, interaction)
5471
if err != nil {
55-
return fmt.Errorf("failed to create package manager guard: %w", err)
72+
ui.Fatalf("Failed to create package manager guard: %s", err)
5673
}
5774

58-
return proxy.Run(ctx, args)
59-
}
60-
61-
func executeNpmFlow(ctx context.Context, config config.Config, args []string) error {
62-
packageManager, err := packagemanager.NewNpmPackageManager(packagemanager.DefaultNpmPackageManagerConfig())
63-
if err != nil {
64-
return fmt.Errorf("failed to create npm package manager: %w", err)
65-
}
66-
67-
return executeCommonFlow(ctx, config, packageManager, args)
68-
}
69-
70-
func executePnpmFlow(ctx context.Context, config config.Config, args []string) error {
71-
packageManager, err := packagemanager.NewNpmPackageManager(packagemanager.DefaultPnpmPackageManagerConfig())
75+
err = proxy.Run(ctx, args)
7276
if err != nil {
73-
return fmt.Errorf("failed to create pnpm package manager: %w", err)
77+
ui.Fatalf("pmg: failed to execute command: %s", err)
7478
}
7579

76-
return executeCommonFlow(ctx, config, packageManager, args)
80+
return err
7781
}

0 commit comments

Comments
 (0)