Skip to content

Commit a57bbb9

Browse files
committed
feat(pnpm): pnpm v9
1 parent 6ab01b6 commit a57bbb9

File tree

15 files changed

+54945
-0
lines changed

15 files changed

+54945
-0
lines changed

module/pnpm/process.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package pnpm
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"github.com/murphysecurity/murphysec/infra/logctx"
78
"github.com/murphysecurity/murphysec/model"
89
"github.com/murphysecurity/murphysec/module/pnpm/shared"
910
v5 "github.com/murphysecurity/murphysec/module/pnpm/v5"
11+
v9 "github.com/murphysecurity/murphysec/module/pnpm/v9"
1012
"io"
1113
"os"
1214
"path/filepath"
@@ -68,6 +70,12 @@ func processDir(ctx context.Context, dir string) (result processDirResult) {
6870
Name: "",
6971
Dependencies: items,
7072
}}
73+
} else if versionNumber == 9 {
74+
result.trees, e = v9.Parse(ctx, bytes.NewReader(data))
75+
if e != nil {
76+
result.e = fmt.Errorf("v9: %w", e)
77+
return
78+
}
7179
} else {
7280
result.e = fmt.Errorf("unsupported version \"%s\"", version)
7381
return

module/pnpm/v9/parse.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package v9
2+
3+
import (
4+
"context"
5+
"github.com/murphysecurity/murphysec/infra/logctx"
6+
"github.com/murphysecurity/murphysec/model"
7+
"github.com/murphysecurity/murphysec/module/pnpm/shared"
8+
"github.com/samber/lo"
9+
"go.uber.org/zap"
10+
"gopkg.in/yaml.v3"
11+
"io"
12+
"sort"
13+
"strconv"
14+
"strings"
15+
)
16+
17+
type importerDepItem struct {
18+
Specifier string `yaml:"specifier"`
19+
Version string `yaml:"version"`
20+
}
21+
22+
type importerItem struct {
23+
DevDependencies map[string]importerDepItem `yaml:"devDependencies"`
24+
Dependencies map[string]importerDepItem `yaml:"dependencies"`
25+
}
26+
27+
type snapshotItem struct {
28+
Dependencies map[string]string `yaml:"dependencies"`
29+
}
30+
31+
func Parse(ctx context.Context, reader io.Reader) (trees []shared.DepTree, e error) {
32+
var d = yaml.NewDecoder(reader)
33+
var doc struct {
34+
Importers map[string]importerItem `yaml:"importers"`
35+
Snapshots map[string]snapshotItem `yaml:"snapshots"`
36+
}
37+
e = d.Decode(&doc)
38+
if e != nil {
39+
return
40+
}
41+
for path, importer := range doc.Importers {
42+
var c = importerHandlingCtx{
43+
Logger: logctx.Use(ctx).Sugar(),
44+
Snapshot: doc.Snapshots,
45+
Handled: make(map[[2]string]struct{}),
46+
CircularDetectMap: make(map[[2]string]struct{}),
47+
CircularPath: make([][2]string, 0),
48+
}
49+
var deps []model.DependencyItem
50+
for name, obj := range importer.Dependencies {
51+
var r model.DependencyItem
52+
r, e = c.handle(name, obj.Version, true)
53+
if e != nil {
54+
return
55+
}
56+
deps = append(deps, r)
57+
}
58+
c.Handled = make(map[[2]string]struct{})
59+
c.CircularDetectMap = make(map[[2]string]struct{})
60+
c.CircularPath = make([][2]string, 0)
61+
for name, obj := range importer.DevDependencies {
62+
var r model.DependencyItem
63+
r, e = c.handle(name, obj.Version, false)
64+
if e != nil {
65+
return
66+
}
67+
deps = append(deps, r)
68+
}
69+
trees = append(trees, shared.DepTree{
70+
Name: path,
71+
Dependencies: deps,
72+
})
73+
}
74+
return
75+
}
76+
77+
type importerHandlingCtx struct {
78+
Logger *zap.SugaredLogger
79+
Snapshot map[string]snapshotItem
80+
Handled map[[2]string]struct{}
81+
CircularDetectMap map[[2]string]struct{}
82+
CircularPath [][2]string
83+
}
84+
85+
func (i *importerHandlingCtx) handle(name, version string, online bool) (d model.DependencyItem, e error) {
86+
var rKey = [2]string{name, version}
87+
var _, circleDetected = i.CircularDetectMap[rKey]
88+
defer func() {
89+
i.CircularPath = i.CircularPath[:len(i.CircularPath)-1]
90+
delete(i.CircularDetectMap, rKey)
91+
}()
92+
i.CircularDetectMap[rKey] = struct{}{}
93+
i.CircularPath = append(i.CircularPath, rKey)
94+
realVersion := splitRealVersionInVersionString(version)
95+
if len(realVersion) > 1 && strings.Contains(realVersion[1:], "@") {
96+
var suffix string
97+
name, version, suffix = splitNameVersion(realVersion)
98+
realVersion = version
99+
version += suffix
100+
}
101+
var searchKey = name + "@" + version
102+
f, ok := i.Snapshot[searchKey]
103+
if !ok {
104+
i.Logger.Warnf("dependency %s not found in snapshot", strconv.Quote(searchKey))
105+
return
106+
}
107+
d.CompName = name
108+
d.CompVersion = splitRealVersionInVersionString(version)
109+
d.Ecosystem = "npm"
110+
d.IsOnline.SetOnline(online)
111+
if _, ok := i.Handled[rKey]; ok || circleDetected {
112+
return
113+
}
114+
i.Handled[rKey] = struct{}{}
115+
var pairs = lo.ToPairs(f.Dependencies)
116+
sort.Slice(pairs, func(i, j int) bool { return pairs[i].Key < pairs[j].Key })
117+
for _, pair := range pairs {
118+
depName := pair.Key
119+
depVersion := pair.Value
120+
var r model.DependencyItem
121+
r, e = i.handle(depName, depVersion, online)
122+
if e != nil {
123+
return
124+
}
125+
d.Dependencies = append(d.Dependencies, r)
126+
}
127+
return
128+
}
129+
130+
func splitRealVersionInVersionString(input string) string {
131+
var i = strings.Index(input, "(")
132+
if i < 0 {
133+
return input
134+
}
135+
return input[:i]
136+
}
137+
138+
func splitNameVersion(input string) (name, version, suffix string) {
139+
if len(input) < 2 {
140+
input = name
141+
return
142+
}
143+
var i = strings.Index(input, "(")
144+
if i >= 0 {
145+
input = input[:i]
146+
suffix = input[i:]
147+
}
148+
var parts = strings.Split(input, "@")
149+
name = parts[0]
150+
if len(parts) > 1 {
151+
version = parts[1]
152+
}
153+
return
154+
}

module/pnpm/v9/parse_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package v9
2+
3+
import (
4+
"context"
5+
"embed"
6+
_ "embed"
7+
"github.com/murphysecurity/murphysec/infra/logctx"
8+
"github.com/murphysecurity/murphysec/utils/must"
9+
"github.com/repeale/fp-go"
10+
"github.com/stretchr/testify/assert"
11+
"go.uber.org/zap"
12+
"io"
13+
"io/fs"
14+
"testing"
15+
)
16+
17+
//go:embed testdata/*
18+
var data embed.FS
19+
20+
func doTest0(reader io.Reader, t *testing.T) struct{} {
21+
tree, e := Parse(logctx.With(context.Background(), must.A(zap.NewDevelopment())), reader)
22+
assert.NoError(t, e)
23+
assert.NotNil(t, tree)
24+
return struct{}{}
25+
}
26+
27+
func void[T, R any](f func(T) R) func(T) {
28+
return func(t T) { f(t) }
29+
}
30+
31+
func TestParse(t *testing.T) {
32+
entries, e := data.ReadDir("testdata")
33+
assert.NoError(t, e)
34+
assert.NotEmpty(t, entries)
35+
e = fs.WalkDir(data, ".", func(path string, d fs.DirEntry, e error) error {
36+
assert.NoError(t, e)
37+
if d.IsDir() {
38+
return nil
39+
}
40+
t.Log(path)
41+
f, e := data.Open(path)
42+
assert.NoError(t, e)
43+
t.Run(path, void(fp.Curry2(doTest0)(f)))
44+
e = f.Close()
45+
assert.NoError(t, e)
46+
return e
47+
})
48+
assert.NoError(t, e)
49+
}
50+
51+
func Test_splitNameVersion(t *testing.T) {
52+
53+
assert.Equal(t, "typescript-eslint", n)
54+
assert.Equal(t, "8.3.0", v)
55+
}
56+
57+
func Test_splitRealVersion(t *testing.T) {
58+
assert.Equal(t, "8.3.0", splitRealVersionInVersionString("8.3.0([email protected]([email protected]))([email protected])"))
59+
}

0 commit comments

Comments
 (0)