mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* define contract for package executors * introduce npx executor * add npx and pnpx cmd support * fix typo * rm PackageExecutor and depend on PackageManager interface * add support for PTY to handle parent-child process interaction * refactor PTY handling in proxy flow * enforce interactiveSession interface check * close reader explicitly and clean npm version for pkg executors * rm interaction from interceptors * add docs and wait for outputRouter before exit * add support for non interactive TTY for proxy mode * add support for CI env var check for non interactive tty proxy mode * update readme to include npx, pnpx support * Update internal/flows/proxy_flow.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> * update ptyx lib * fix docs typo --------- Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
209 lines
6.3 KiB
Go
209 lines
6.3 KiB
Go
package interceptors
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
|
"github.com/safedep/pmg/analyzer"
|
|
"github.com/safedep/pmg/guard"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func mockPackageVersion(name, version string) *packagev1.PackageVersion {
|
|
return &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{Name: name},
|
|
Version: version,
|
|
}
|
|
}
|
|
|
|
func mockAnalysisResult() *analyzer.PackageVersionAnalysisResult {
|
|
return &analyzer.PackageVersionAnalysisResult{
|
|
Action: analyzer.ActionConfirm,
|
|
Summary: "Test suspicious package",
|
|
}
|
|
}
|
|
|
|
func TestHandleConfirmationRequests(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
confirmationResponse bool
|
|
confirmationError error
|
|
beforeInteractionErr error
|
|
afterInteractionErr error
|
|
useNilHooks bool
|
|
expectedResponse bool
|
|
verifyHooksCalled bool
|
|
}{
|
|
{
|
|
name: "user confirms installation",
|
|
confirmationResponse: true,
|
|
confirmationError: nil,
|
|
useNilHooks: false,
|
|
expectedResponse: true,
|
|
verifyHooksCalled: true,
|
|
},
|
|
{
|
|
name: "user denies installation",
|
|
confirmationResponse: false,
|
|
confirmationError: nil,
|
|
useNilHooks: false,
|
|
expectedResponse: false,
|
|
verifyHooksCalled: true,
|
|
},
|
|
{
|
|
name: "confirmation error returns false",
|
|
confirmationResponse: false,
|
|
confirmationError: errors.New("confirmation failed"),
|
|
useNilHooks: false,
|
|
expectedResponse: false,
|
|
verifyHooksCalled: true,
|
|
},
|
|
{
|
|
name: "before interaction hook error is non-fatal",
|
|
confirmationResponse: true,
|
|
confirmationError: nil,
|
|
beforeInteractionErr: errors.New("before hook failed"),
|
|
useNilHooks: false,
|
|
expectedResponse: true,
|
|
verifyHooksCalled: true,
|
|
},
|
|
{
|
|
name: "after interaction hook error is non-fatal",
|
|
confirmationResponse: true,
|
|
confirmationError: nil,
|
|
afterInteractionErr: errors.New("after hook failed"),
|
|
useNilHooks: false,
|
|
expectedResponse: true,
|
|
verifyHooksCalled: true,
|
|
},
|
|
{
|
|
name: "nil hooks does not panic",
|
|
confirmationResponse: true,
|
|
confirmationError: nil,
|
|
useNilHooks: true,
|
|
expectedResponse: true,
|
|
verifyHooksCalled: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
beforeCalled := false
|
|
afterCalled := false
|
|
var afterConfirmedParam bool
|
|
|
|
interaction := guard.PackageManagerGuardInteraction{
|
|
GetConfirmationOnMalware: func(results []*analyzer.PackageVersionAnalysisResult) (bool, error) {
|
|
assert.Len(t, results, 1)
|
|
return tt.confirmationResponse, tt.confirmationError
|
|
},
|
|
}
|
|
|
|
var hooks *ConfirmationHook
|
|
if !tt.useNilHooks {
|
|
hooks = &ConfirmationHook{
|
|
BeforeInteraction: func(results []*analyzer.PackageVersionAnalysisResult) error {
|
|
beforeCalled = true
|
|
assert.Len(t, results, 1)
|
|
return tt.beforeInteractionErr
|
|
},
|
|
AfterInteraction: func(results []*analyzer.PackageVersionAnalysisResult, confirmed bool) error {
|
|
afterCalled = true
|
|
afterConfirmedParam = confirmed
|
|
assert.Len(t, results, 1)
|
|
return tt.afterInteractionErr
|
|
},
|
|
}
|
|
}
|
|
|
|
confirmationChan := make(chan *ConfirmationRequest, 1)
|
|
go HandleConfirmationRequests(confirmationChan, &interaction, hooks)
|
|
|
|
pkgVersion := mockPackageVersion("test-package", "1.0.0")
|
|
analysisResult := mockAnalysisResult()
|
|
analysisResult.PackageVersion = pkgVersion
|
|
req := NewConfirmationRequest(pkgVersion, analysisResult)
|
|
confirmationChan <- req
|
|
|
|
response := <-req.ResponseChan
|
|
|
|
assert.Equal(t, tt.expectedResponse, response)
|
|
|
|
if tt.verifyHooksCalled {
|
|
assert.True(t, beforeCalled, "BeforeInteraction hook should be called")
|
|
assert.True(t, afterCalled, "AfterInteraction hook should be called")
|
|
if tt.confirmationError == nil {
|
|
assert.Equal(t, tt.confirmationResponse, afterConfirmedParam,
|
|
"AfterInteraction should receive correct confirmation status")
|
|
}
|
|
} else {
|
|
assert.False(t, beforeCalled, "BeforeInteraction hook should not be called when hooks are nil")
|
|
assert.False(t, afterCalled, "AfterInteraction hook should not be called when hooks are nil")
|
|
}
|
|
|
|
close(confirmationChan)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandleConfirmationRequests_MultipleSequential(t *testing.T) {
|
|
processedPackages := []string{}
|
|
|
|
interaction := guard.PackageManagerGuardInteraction{
|
|
GetConfirmationOnMalware: func(results []*analyzer.PackageVersionAnalysisResult) (bool, error) {
|
|
pkgName := results[0].PackageVersion.GetPackage().GetName()
|
|
processedPackages = append(processedPackages, pkgName)
|
|
return true, nil
|
|
},
|
|
}
|
|
|
|
confirmationChan := make(chan *ConfirmationRequest, 3)
|
|
go HandleConfirmationRequests(confirmationChan, &interaction, nil)
|
|
|
|
pkgVersion1 := mockPackageVersion("package-1", "1.0.0")
|
|
analysisResult1 := mockAnalysisResult()
|
|
analysisResult1.PackageVersion = pkgVersion1
|
|
req1 := NewConfirmationRequest(pkgVersion1, analysisResult1)
|
|
|
|
pkgVersion2 := mockPackageVersion("package-2", "1.0.0")
|
|
analysisResult2 := mockAnalysisResult()
|
|
analysisResult2.PackageVersion = pkgVersion2
|
|
req2 := NewConfirmationRequest(pkgVersion2, analysisResult2)
|
|
|
|
pkgVersion3 := mockPackageVersion("package-3", "1.0.0")
|
|
analysisResult3 := mockAnalysisResult()
|
|
analysisResult3.PackageVersion = pkgVersion3
|
|
req3 := NewConfirmationRequest(pkgVersion3, analysisResult3)
|
|
|
|
confirmationChan <- req1
|
|
confirmationChan <- req2
|
|
confirmationChan <- req3
|
|
|
|
response1 := <-req1.ResponseChan
|
|
response2 := <-req2.ResponseChan
|
|
response3 := <-req3.ResponseChan
|
|
|
|
assert.True(t, response1)
|
|
assert.True(t, response2)
|
|
assert.True(t, response3)
|
|
|
|
assert.Equal(t, []string{"package-1", "package-2", "package-3"}, processedPackages)
|
|
|
|
close(confirmationChan)
|
|
}
|
|
|
|
func TestNewConfirmationRequest(t *testing.T) {
|
|
pkgVersion := mockPackageVersion("test-package", "1.0.0")
|
|
analysisResult := mockAnalysisResult()
|
|
analysisResult.PackageVersion = pkgVersion
|
|
|
|
req := NewConfirmationRequest(pkgVersion, analysisResult)
|
|
|
|
assert.NotNil(t, req)
|
|
assert.Equal(t, pkgVersion, req.PackageVersion)
|
|
assert.Equal(t, analysisResult, req.AnalysisResult)
|
|
assert.NotNil(t, req.ResponseChan)
|
|
assert.Equal(t, 1, cap(req.ResponseChan), "ResponseChan should have buffer size of 1")
|
|
}
|