Skip to content

Commit 97f315d

Browse files
authored
Merge pull request #2 from zph/add-wasm-plugins
Working wasm plugin model
2 parents 31facc1 + 76c1672 commit 97f315d

File tree

12 files changed

+286
-18
lines changed

12 files changed

+286
-18
lines changed

cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
)
1313

1414
var (
15-
version = "v0.0.1"
15+
version = "v100.0.1"
1616
commit = ""
1717
date = ""
1818
)

examples/simple.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,18 @@ rules:
7171
scope: path
7272
name: fn
7373
body: const fn = (path, _i, _l) => path.includes('print')
74+
75+
- id: no-python-in-path
76+
description: Don't use python here
77+
recommendation: None
78+
severity: low
79+
link: https://examples.com/wiki/no-print-js-path-level
80+
include_paths: '\.py$'
81+
exclude_paths: null
82+
fn:
83+
type: wasm
84+
scope: path
85+
name: path_validator
86+
body: ./plugins/test-plugin.wasm
87+
metadata:
88+
sha256: f2880b9d1a2f70f7eddca65c7aa539483c800653669e5a070b8cb3b11a199eca

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ require (
1313

1414
require (
1515
github.com/dlclark/regexp2 v1.7.0 // indirect
16+
github.com/extism/go-sdk v1.2.0 // indirect
1617
github.com/fsnotify/fsnotify v1.7.0 // indirect
1718
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
19+
github.com/gobwas/glob v0.2.3 // indirect
1820
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
1921
github.com/hashicorp/hcl v1.0.0 // indirect
2022
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -30,8 +32,10 @@ require (
3032
github.com/spf13/cast v1.6.0 // indirect
3133
github.com/spf13/pflag v1.0.5 // indirect
3234
github.com/subosito/gotenv v1.6.0 // indirect
35+
github.com/tetratelabs/wazero v1.3.0 // indirect
3336
go.uber.org/atomic v1.9.0 // indirect
34-
go.uber.org/multierr v1.9.0 // indirect
37+
go.uber.org/multierr v1.10.0 // indirect
38+
go.uber.org/zap v1.27.0 // indirect
3539
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
3640
golang.org/x/sys v0.17.0 // indirect
3741
golang.org/x/text v0.14.0 // indirect

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 h1:O7I1iuzEA7SG+dK8ocO
1515
github.com/dop251/goja v0.0.0-20240220182346-e401ed450204/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
1616
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
1717
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
18+
github.com/extism/go-sdk v1.2.0 h1:A0DnIMthdP8h6K9NbRpRs1PIXHOUlb/t/TZWk5eUzx4=
19+
github.com/extism/go-sdk v1.2.0/go.mod h1:xUfKSEQndAvHBc1Ohdre0e+UdnRzUpVfbA8QLcx4fbY=
1820
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
1921
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
2022
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
2123
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
2224
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
2325
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
26+
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
27+
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
2428
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
2529
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
2630
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
@@ -84,11 +88,17 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
8488
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
8589
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
8690
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
91+
github.com/tetratelabs/wazero v1.3.0 h1:nqw7zCldxE06B8zSZAY0ACrR9OH5QCcPwYmYlwtcwtE=
92+
github.com/tetratelabs/wazero v1.3.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
8793
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
8894
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
8995
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
9096
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
9197
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
98+
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
99+
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
100+
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
101+
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
92102
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
93103
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
94104
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=

main_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ func TestProcessFile(t *testing.T) {
4040
findingsCount int
4141
expectedErr error
4242
}{
43-
{"Basic test without ignores", `print("A")`, "example.py", 4, nil},
44-
{"Basic test for-file ignore", forFileIgnore, "example.py", 2, nil},
45-
{"Basic test next-line ignore", nextLineIgnore, "example.py", 2, nil},
46-
{"Basic test next-line ignore shorthand", nextLineIgnoreShorthand, "example.py", 3, nil},
47-
{"Basic test next-line ignore doesn't apply", nextLineIgnoreDoesntApply, "example.py", 4, nil},
48-
{"Basic test with faulty ignore statement", fileWithFaultyIgnoreStatement, "example.py", 4, nil},
49-
{"Basic test with banned filename", nextLineIgnore, "print.py", 3, nil},
43+
{"Basic test without ignores", `print("A")`, "example.py", 5, nil},
44+
{"Basic test for-file ignore", forFileIgnore, "example.py", 3, nil},
45+
{"Basic test next-line ignore", nextLineIgnore, "example.py", 3, nil},
46+
{"Basic test next-line ignore shorthand", nextLineIgnoreShorthand, "example.py", 4, nil},
47+
{"Basic test next-line ignore doesn't apply", nextLineIgnoreDoesntApply, "example.py", 5, nil},
48+
{"Basic test with faulty ignore statement", fileWithFaultyIgnoreStatement, "example.py", 5, nil},
49+
{"Basic test with banned filename", nextLineIgnore, "print.py", 4, nil},
5050
}
5151
for idx, tt := range tests {
5252
t.Run(tt.name, func(t *testing.T) {
@@ -86,7 +86,7 @@ func TestConfigFileParsing(t *testing.T) {
8686
content string
8787
expectedRuleCount int
8888
}{
89-
{"basic config file with 1 rule", simpleConfigFile, 6},
89+
{"basic config file with 1 rule", simpleConfigFile, 7},
9090
}
9191
for idx, tt := range tests {
9292
t.Run(tt.name, func(t *testing.T) {

pkg/log.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package polylint
2+
3+
import "go.uber.org/zap"
4+
5+
var logz *zap.SugaredLogger
6+
7+
func init() {
8+
logger, _ := zap.NewProductionConfig().Build(
9+
zap.WithCaller(true),
10+
)
11+
12+
zap.NewAtomicLevelAt(zap.DebugLevel)
13+
defer logger.Sync() // flushes buffer, if any
14+
logz = logger.Sugar()
15+
logz.Debugw("polylint initialized", "version", "0.0.2")
16+
}

pkg/processors.go

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

33
import (
4+
"context"
45
"crypto/sha256"
6+
"encoding/json"
57
"fmt"
68
"io"
79
"log"
@@ -13,6 +15,7 @@ import (
1315
"strings"
1416

1517
"github.com/dop251/goja"
18+
extism "github.com/extism/go-sdk"
1619
"github.com/spf13/viper"
1720
"golang.org/x/mod/semver"
1821
"gopkg.in/yaml.v2"
@@ -41,7 +44,7 @@ func extractIgnoresFromLine(line string, lineNo int, f *FileReport) error {
4144
f.Ignores = append(f.Ignores, Ignore{Scope: pathScope, SourceLineNo: lineNo, LineNo: 0, Id: ignore})
4245
}
4346
} else {
44-
fmt.Printf("WARNING: directive for polylint not recognized on line %d %s %s\n", lineNo, directive, ignoresStr)
47+
logz.Warnf("WARNING: directive for polylint not recognized on line %d %s %s\n", lineNo, directive, ignoresStr)
4548
return nil
4649
}
4750
}
@@ -98,24 +101,27 @@ func LoadConfigFile(content string) (ConfigFile, error) {
98101
var config ConfigFile
99102
err := yaml.Unmarshal([]byte(content), &rawConfig)
100103
if err != nil {
101-
fmt.Printf("Error unmarshalling YAML: %v", err)
104+
logz.Errorf("Error unmarshalling YAML: %v", err)
102105
return ConfigFile{}, err
103106
}
104107

105108
if !strings.HasPrefix(rawConfig.Version, "v") {
106-
fmt.Printf("Error: config file version must start with a 'v' but was %s\n", rawConfig.Version)
109+
logz.Errorf("Error: config file version must start with a 'v' but was %s\n", rawConfig.Version)
107110
panic("Invalid version due to semver incompatibility")
108111
}
109112

110113
if !semver.IsValid(rawConfig.Version) {
111-
fmt.Printf("Error: Config version %s is newer than binary version %s\n", rawConfig.Version, viper.GetString("binary_version"))
112-
fmt.Println(semver.IsValid(rawConfig.Version))
114+
logz.Errorf("Error: Config version %s is newer than binary version %s\n", rawConfig.Version, viper.GetString("binary_version"))
115+
logz.Errorln(semver.IsValid(rawConfig.Version))
113116
panic("Invalid version due to semver incompatibility")
114117
}
115118

116119
// If version file is too new for binary version
117120
if semver.Compare(rawConfig.Version, viper.GetString("binary_version")) == 1 {
118-
fmt.Printf("Warning: config file version %s is newer than binary version %s\n", rawConfig.Version, viper.GetString("binary_version"))
121+
_ = 1
122+
// TODO: determine how to handle version for version check when not set in tests
123+
// Ignore for now until we can control the output
124+
//logz.Warnf("Warning: config file version %s is newer than binary version %s\n", rawConfig.Version, viper.GetString("binary_version"))
119125
}
120126

121127
config.Version = rawConfig.Version
@@ -285,6 +291,8 @@ func BuildLineFn(f RawFn) RuleFunc {
285291
return buildLineFnBuiltin(f)
286292
case "js":
287293
return buildJsFn(f)
294+
case "wasm":
295+
return buildWasmFn(f)
288296
default:
289297
panic(fmt.Sprintf("unknown type %s", f.Type))
290298
}
@@ -296,6 +304,8 @@ func BuildFileScopeFn(f RawFn) RuleFunc {
296304
return BuildFileFnBuiltin(f)
297305
case "js":
298306
return BuildFileFnJs(f)
307+
case "wasm":
308+
return buildWasmFn(f)
299309
default:
300310
panic(fmt.Sprintf("unknown type %s", f.Type))
301311
}
@@ -340,6 +350,8 @@ func BuildPathScopeFn(f RawFn) RuleFunc {
340350
return BuildPathFnBuiltin(f)
341351
case "js":
342352
return BuildPathFnJs(f)
353+
case "wasm":
354+
return buildWasmFn(f)
343355
default:
344356
panic(fmt.Sprintf("unknown type %s", f.Type))
345357
}
@@ -394,6 +406,80 @@ func buildJsFn(f RawFn) RuleFunc {
394406
return fn
395407
}
396408

409+
func buildWasmFn(f RawFn) RuleFunc {
410+
hash, err := f.GetMetadataHash()
411+
if err != nil {
412+
logz.Warnf("Warning: cannot find metadata hash for %s\n", f.Body)
413+
}
414+
var content []byte
415+
416+
// TODO: handle null case of hash
417+
if hash != "" {
418+
content, err = f.GetWASMFromCache(hash)
419+
}
420+
if err != nil {
421+
if strings.HasPrefix(f.Body, "http") {
422+
content, err = f.GetWASMFromUrl(f.Body)
423+
} else {
424+
content, err = f.GetWASMFromPath(f.Body)
425+
}
426+
}
427+
if err != nil {
428+
panic(err)
429+
}
430+
431+
ok := f.CheckWASMHash(content, hash)
432+
if !ok && hash != "" {
433+
logz.Errorf("hash mismatch for %s", f.Body)
434+
}
435+
f.WriteWASMToCache(content)
436+
437+
var location []extism.Wasm
438+
location = append(location, extism.WasmData{
439+
Data: content,
440+
Hash: hash,
441+
})
442+
443+
manifest := extism.Manifest{
444+
Wasm: location,
445+
}
446+
447+
ctx := context.Background()
448+
config := extism.PluginConfig{
449+
EnableWasi: true,
450+
}
451+
452+
plugin, err := extism.NewPlugin(ctx, manifest, config, []extism.HostFunction{})
453+
if err != nil {
454+
logz.Errorf("Failed to initialize plugin: %v\n", err)
455+
os.Exit(1)
456+
}
457+
458+
return func(path string, idx int, line string) bool {
459+
args := RuleFuncArgs{path, idx, line}
460+
b, err := json.Marshal(&args)
461+
if err != nil {
462+
panic(err)
463+
}
464+
465+
exit, bytes, err := plugin.CallWithContext(ctx, f.Name, b)
466+
if err != nil {
467+
logz.Errorln(err)
468+
os.Exit(int(exit))
469+
}
470+
var result RuleFuncResult
471+
json.Unmarshal(bytes, &result)
472+
473+
return result.Value
474+
}
475+
}
476+
477+
type RuleFuncArgs [3]interface{}
478+
479+
type RuleFuncResult struct {
480+
Value bool
481+
}
482+
397483
func ProcessFile(content string, path string, cfg ConfigFile) (FileReport, error) {
398484
f := FileReport{
399485
Path: path,
@@ -405,7 +491,7 @@ func ProcessFile(content string, path string, cfg ConfigFile) (FileReport, error
405491
for idx, line := range lines {
406492
err := processLine(line, idx, &f)
407493
if err != nil {
408-
fmt.Printf("ERROR: %s\n", err)
494+
logz.Errorf("ERROR: %s\n", err)
409495
return FileReport{}, err
410496
}
411497
}

0 commit comments

Comments
 (0)