@synopackageland/cli 0.34.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +6 -3
- package/assets/go/route-audit/main.go +549 -45
- package/dist/build-spk/install-wizard.d.ts +2 -0
- package/dist/build-spk/install-wizard.js +4 -0
- package/dist/build-spk/wizard-messages.d.ts +22 -0
- package/dist/build-spk/wizard-messages.js +22 -0
- package/dist/cli.js +26 -0
- package/dist/error-registry.js +10 -0
- package/dist/standalone/api-description.js +3 -0
- package/dist/standalone/api-permissions.d.ts +4 -1
- package/dist/standalone/api-permissions.js +10 -4
- package/dist/standalone/build.js +15 -2
- package/dist/standalone/inspect-spk.d.ts +2 -0
- package/dist/standalone/inspect-spk.js +16 -10
- package/dist/standalone/manifest.d.ts +6 -0
- package/dist/standalone/manifest.js +40 -5
- package/dist/standalone/route-audit.d.ts +9 -3
- package/dist/standalone/route-audit.js +6 -2
- package/dist/standalone/shared-folder.d.ts +6 -1
- package/dist/standalone/shared-folder.js +13 -0
- package/dist/standalone/spk.js +5 -6
- package/package.json +1 -1
- package/skills/SKILL.md +43 -3
|
@@ -2,26 +2,436 @@
|
|
|
2
2
|
package main
|
|
3
3
|
|
|
4
4
|
import (
|
|
5
|
+
"bytes"
|
|
5
6
|
"encoding/json"
|
|
6
7
|
"fmt"
|
|
7
8
|
"go/ast"
|
|
8
9
|
"go/parser"
|
|
9
10
|
"go/token"
|
|
11
|
+
"io"
|
|
10
12
|
"os"
|
|
13
|
+
"os/exec"
|
|
11
14
|
"path/filepath"
|
|
15
|
+
"regexp"
|
|
16
|
+
"sort"
|
|
12
17
|
"strconv"
|
|
13
18
|
"strings"
|
|
14
19
|
)
|
|
15
20
|
|
|
16
21
|
type input struct {
|
|
17
22
|
Dir string
|
|
23
|
+
Roots []string
|
|
24
|
+
Arches []string
|
|
18
25
|
Permissions map[string]string
|
|
19
26
|
SDK map[string]map[string]string
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
type result struct {
|
|
23
|
-
Problems
|
|
24
|
-
EntryGate
|
|
30
|
+
Problems []string `json:"problems"`
|
|
31
|
+
EntryGate bool `json:"entryGate"`
|
|
32
|
+
HTTPRoutes []httpRoute `json:"httpRoutes"`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type httpRoute struct {
|
|
36
|
+
Prefix string `json:"prefix"`
|
|
37
|
+
Method string `json:"method"`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sdkModule = "github.com/synopackageland/syno-pkg-land-sdk"
|
|
41
|
+
|
|
42
|
+
// Resolve target build constraints without running any application code.
|
|
43
|
+
func shippedFiles(in input) (map[string]string, error) {
|
|
44
|
+
files := map[string]string{}
|
|
45
|
+
if _, err := os.Stat(filepath.Join(in.Dir, "go.mod")); os.IsNotExist(err) {
|
|
46
|
+
matches, err := filepath.Glob(filepath.Join(in.Dir, "*.go"))
|
|
47
|
+
for _, path := range matches {
|
|
48
|
+
if !strings.HasSuffix(path, "_test.go") {
|
|
49
|
+
files[path] = "fixture"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return files, err
|
|
53
|
+
}
|
|
54
|
+
roots, arches := in.Roots, in.Arches
|
|
55
|
+
if len(roots) == 0 {
|
|
56
|
+
roots = []string{"."}
|
|
57
|
+
}
|
|
58
|
+
if len(arches) == 0 {
|
|
59
|
+
arches = []string{"amd64", "arm64", "arm"}
|
|
60
|
+
}
|
|
61
|
+
for _, arch := range arches {
|
|
62
|
+
cmd := exec.Command("go", append([]string{"list", "-e", "-mod=readonly", "-deps", "-json"}, roots...)...)
|
|
63
|
+
cmd.Dir = in.Dir
|
|
64
|
+
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH="+arch, "GOARM=7", "CGO_ENABLED=0", "GOWORK=off", "GOFLAGS=")
|
|
65
|
+
raw, err := cmd.Output()
|
|
66
|
+
if err != nil {
|
|
67
|
+
return nil, fmt.Errorf("cannot resolve shipped Go import graph")
|
|
68
|
+
}
|
|
69
|
+
d := json.NewDecoder(bytes.NewReader(raw))
|
|
70
|
+
for {
|
|
71
|
+
var p struct {
|
|
72
|
+
Dir, ImportPath string
|
|
73
|
+
GoFiles []string
|
|
74
|
+
Standard bool
|
|
75
|
+
DepOnly bool
|
|
76
|
+
Error *struct{ Err string }
|
|
77
|
+
Module *struct {
|
|
78
|
+
Main bool
|
|
79
|
+
Replace *struct{ Dir string }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
err := d.Decode(&p)
|
|
83
|
+
if err == io.EOF {
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
if err != nil {
|
|
87
|
+
return nil, err
|
|
88
|
+
}
|
|
89
|
+
if !p.DepOnly && p.Error != nil {
|
|
90
|
+
return nil, fmt.Errorf("cannot resolve a configured shipped Go root")
|
|
91
|
+
}
|
|
92
|
+
if p.Standard || p.ImportPath == sdkModule || strings.HasPrefix(p.ImportPath, sdkModule+"/") {
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if p.Module == nil || !p.Module.Main && p.Module.Replace == nil {
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
for _, name := range p.GoFiles {
|
|
99
|
+
files[filepath.Join(p.Dir, name)] = p.ImportPath
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if len(files) == 0 {
|
|
104
|
+
return nil, fmt.Errorf("shipped Go roots contain no auditable source")
|
|
105
|
+
}
|
|
106
|
+
return files, nil
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func stringLiteral(expr ast.Expr) string {
|
|
110
|
+
if v, ok := expr.(*ast.BasicLit); ok && v.Kind == token.STRING {
|
|
111
|
+
s, _ := strconv.Unquote(v.Value)
|
|
112
|
+
return s
|
|
113
|
+
}
|
|
114
|
+
return ""
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type source struct {
|
|
118
|
+
file *ast.File
|
|
119
|
+
path, pkg string
|
|
120
|
+
imports map[string]string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Follow the handler argument through local declarations and factories. A
|
|
124
|
+
// boundary never exempts an entire directory, file, or any listener primitive.
|
|
125
|
+
func boundaryHandlers(sources []source) map[ast.Node]bool {
|
|
126
|
+
decls := map[string][]ast.Node{}
|
|
127
|
+
owners := map[ast.Node]source{}
|
|
128
|
+
for _, s := range sources {
|
|
129
|
+
ast.Inspect(s.file, func(n ast.Node) bool {
|
|
130
|
+
if n != nil {
|
|
131
|
+
owners[n] = s
|
|
132
|
+
}
|
|
133
|
+
return true
|
|
134
|
+
})
|
|
135
|
+
for _, decl := range s.file.Decls {
|
|
136
|
+
switch d := decl.(type) {
|
|
137
|
+
case *ast.FuncDecl:
|
|
138
|
+
key := s.pkg + "." + d.Name.Name
|
|
139
|
+
if d.Recv != nil {
|
|
140
|
+
t := d.Recv.List[0].Type
|
|
141
|
+
if v, ok := t.(*ast.StarExpr); ok {
|
|
142
|
+
t = v.X
|
|
143
|
+
}
|
|
144
|
+
if v, ok := t.(*ast.Ident); ok {
|
|
145
|
+
key = s.pkg + "." + v.Name
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
decls[key] = append(decls[key], d)
|
|
149
|
+
case *ast.GenDecl:
|
|
150
|
+
for _, spec := range d.Specs {
|
|
151
|
+
switch v := spec.(type) {
|
|
152
|
+
case *ast.TypeSpec:
|
|
153
|
+
decls[s.pkg+"."+v.Name.Name] = append(decls[s.pkg+"."+v.Name.Name], v)
|
|
154
|
+
case *ast.ValueSpec:
|
|
155
|
+
for _, name := range v.Names {
|
|
156
|
+
decls[s.pkg+"."+name.Name] = append(decls[s.pkg+"."+name.Name], v)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
safe, visited := map[ast.Node]bool{}, map[ast.Node]bool{}
|
|
164
|
+
var mark func(ast.Node)
|
|
165
|
+
mark = func(root ast.Node) {
|
|
166
|
+
if root == nil || visited[root] {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
visited[root] = true
|
|
170
|
+
s := owners[root]
|
|
171
|
+
ast.Inspect(root, func(n ast.Node) bool {
|
|
172
|
+
if n == nil {
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
safe[n] = true
|
|
176
|
+
key := ""
|
|
177
|
+
switch v := n.(type) {
|
|
178
|
+
case *ast.Ident:
|
|
179
|
+
key = s.pkg + "." + v.Name
|
|
180
|
+
if v.Obj != nil {
|
|
181
|
+
// A parameter or receiver carries a value built elsewhere.
|
|
182
|
+
// Following its type would admit every method of that type,
|
|
183
|
+
// such as an App Config whose method value is the handler.
|
|
184
|
+
if _, param := v.Obj.Decl.(*ast.Field); param {
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
if d, ok := v.Obj.Decl.(ast.Node); ok {
|
|
188
|
+
mark(d)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
case *ast.SelectorExpr:
|
|
192
|
+
if id, ok := v.X.(*ast.Ident); ok {
|
|
193
|
+
key = s.imports[id.Name] + "." + v.Sel.Name
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for _, decl := range decls[key] {
|
|
197
|
+
mark(decl)
|
|
198
|
+
}
|
|
199
|
+
return true
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
for _, s := range sources {
|
|
203
|
+
ast.Inspect(s.file, func(n ast.Node) bool {
|
|
204
|
+
call, ok := n.(*ast.CallExpr)
|
|
205
|
+
if ok && len(call.Args) == 2 {
|
|
206
|
+
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
207
|
+
if id, ok := sel.X.(*ast.Ident); ok && s.imports[id.Name] == sdkModule+"/api" && sel.Sel.Name == "HandleIntegration" {
|
|
208
|
+
mark(call.Args[1])
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if !ok || len(call.Args) != 3 {
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
216
|
+
if !ok {
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
id, ok := sel.X.(*ast.Ident)
|
|
220
|
+
if ok && (s.imports[id.Name] == sdkModule+"/api" && sel.Sel.Name == "HandleHTTP" ||
|
|
221
|
+
s.imports[id.Name] == sdkModule+"/ipc" && (sel.Sel.Name == "ServeUnix" || sel.Sel.Name == "StartUnix")) {
|
|
222
|
+
mark(call.Args[2])
|
|
223
|
+
}
|
|
224
|
+
return true
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
return safe
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// A handler admitted to a private/raw boundary cannot also be installed as
|
|
231
|
+
// the CGI root. SDK server construction ends this search because it gates
|
|
232
|
+
// its registered routes before invoking their handlers.
|
|
233
|
+
func reusesBoundary(expr ast.Node, owner source, sources []source, safe map[ast.Node]bool, seen map[ast.Node]bool) bool {
|
|
234
|
+
if expr == nil || seen[expr] {
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
seen[expr] = true
|
|
238
|
+
imports := owner.imports
|
|
239
|
+
if call, ok := expr.(*ast.CallExpr); ok {
|
|
240
|
+
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
241
|
+
if id, ok := sel.X.(*ast.Ident); ok && imports[id.Name] == sdkModule+"/api" {
|
|
242
|
+
switch sel.Sel.Name {
|
|
243
|
+
case "NewServerWithPermissions":
|
|
244
|
+
return false
|
|
245
|
+
case "WithRequestDeadline", "WithDescription":
|
|
246
|
+
return len(call.Args) > 0 && reusesBoundary(call.Args[0], owner, sources, safe, seen)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
found := false
|
|
252
|
+
ast.Inspect(expr, func(n ast.Node) bool {
|
|
253
|
+
if n == nil || found {
|
|
254
|
+
return false
|
|
255
|
+
}
|
|
256
|
+
if n != expr {
|
|
257
|
+
if _, ok := n.(*ast.CallExpr); ok {
|
|
258
|
+
found = reusesBoundary(n, owner, sources, safe, seen)
|
|
259
|
+
return false
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
targetPackage, targetName := "", ""
|
|
263
|
+
if id, ok := n.(*ast.Ident); ok {
|
|
264
|
+
if id.Obj != nil {
|
|
265
|
+
if decl, ok := id.Obj.Decl.(ast.Node); ok {
|
|
266
|
+
found = safe[decl] || reusesBoundary(decl, owner, sources, safe, seen)
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
targetPackage, targetName = owner.pkg, id.Name
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if selector, ok := n.(*ast.SelectorExpr); ok {
|
|
273
|
+
if pkg, ok := selector.X.(*ast.Ident); ok {
|
|
274
|
+
targetPackage, targetName = imports[pkg.Name], selector.Sel.Name
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if targetPackage != "" {
|
|
278
|
+
for _, candidate := range sources {
|
|
279
|
+
if candidate.pkg != targetPackage {
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
for _, decl := range candidate.file.Decls {
|
|
283
|
+
var target ast.Node
|
|
284
|
+
switch d := decl.(type) {
|
|
285
|
+
case *ast.FuncDecl:
|
|
286
|
+
if d.Recv == nil && d.Name.Name == targetName {
|
|
287
|
+
target = d
|
|
288
|
+
}
|
|
289
|
+
case *ast.GenDecl:
|
|
290
|
+
for _, spec := range d.Specs {
|
|
291
|
+
switch v := spec.(type) {
|
|
292
|
+
case *ast.TypeSpec:
|
|
293
|
+
if v.Name.Name == targetName {
|
|
294
|
+
target = v
|
|
295
|
+
}
|
|
296
|
+
case *ast.ValueSpec:
|
|
297
|
+
for _, name := range v.Names {
|
|
298
|
+
if name.Name == targetName {
|
|
299
|
+
target = v
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if target != nil && (safe[target] || reusesBoundary(target, candidate, sources, safe, seen)) {
|
|
306
|
+
found = true
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return !found
|
|
312
|
+
})
|
|
313
|
+
return found
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Resolve declared receiver types without executing the App or requiring a
|
|
317
|
+
// successful type check (the audit also runs while projects are being edited).
|
|
318
|
+
func receiverType(expr ast.Node, src source, sources []source, seen map[ast.Node]bool) string {
|
|
319
|
+
if expr == nil || seen[expr] {
|
|
320
|
+
return ""
|
|
321
|
+
}
|
|
322
|
+
seen[expr] = true
|
|
323
|
+
follow := func(n ast.Node) string { return receiverType(n, src, sources, seen) }
|
|
324
|
+
find := func(pkg, name string) (ast.Node, source) {
|
|
325
|
+
for _, s := range sources {
|
|
326
|
+
if s.pkg != pkg {
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
for _, decl := range s.file.Decls {
|
|
330
|
+
if fn, ok := decl.(*ast.FuncDecl); ok && fn.Recv == nil && fn.Name.Name == name {
|
|
331
|
+
return fn, s
|
|
332
|
+
}
|
|
333
|
+
if gen, ok := decl.(*ast.GenDecl); ok {
|
|
334
|
+
for _, spec := range gen.Specs {
|
|
335
|
+
if typ, ok := spec.(*ast.TypeSpec); ok && typ.Name.Name == name {
|
|
336
|
+
return typ, s
|
|
337
|
+
}
|
|
338
|
+
if value, ok := spec.(*ast.ValueSpec); ok {
|
|
339
|
+
for _, id := range value.Names {
|
|
340
|
+
if id.Name == name {
|
|
341
|
+
return id, s
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return nil, src
|
|
350
|
+
}
|
|
351
|
+
switch v := expr.(type) {
|
|
352
|
+
case *ast.ParenExpr:
|
|
353
|
+
return follow(v.X)
|
|
354
|
+
case *ast.StarExpr:
|
|
355
|
+
return follow(v.X)
|
|
356
|
+
case *ast.UnaryExpr:
|
|
357
|
+
return follow(v.X)
|
|
358
|
+
case *ast.CompositeLit:
|
|
359
|
+
return follow(v.Type)
|
|
360
|
+
case *ast.SelectorExpr:
|
|
361
|
+
if id, ok := v.X.(*ast.Ident); ok && src.imports[id.Name] != "" {
|
|
362
|
+
return src.imports[id.Name] + "." + v.Sel.Name
|
|
363
|
+
}
|
|
364
|
+
typ := follow(v.X)
|
|
365
|
+
if dot := strings.LastIndex(typ, "."); dot > 0 {
|
|
366
|
+
decl, owner := find(typ[:dot], typ[dot+1:])
|
|
367
|
+
if d, ok := decl.(*ast.TypeSpec); ok {
|
|
368
|
+
if st, ok := d.Type.(*ast.StructType); ok {
|
|
369
|
+
for _, field := range st.Fields.List {
|
|
370
|
+
for _, name := range field.Names {
|
|
371
|
+
if name.Name == v.Sel.Name {
|
|
372
|
+
return receiverType(field.Type, owner, sources, seen)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
case *ast.Ident:
|
|
380
|
+
if v.Obj != nil {
|
|
381
|
+
switch d := v.Obj.Decl.(type) {
|
|
382
|
+
case *ast.Field:
|
|
383
|
+
return follow(d.Type)
|
|
384
|
+
case *ast.ValueSpec:
|
|
385
|
+
if d.Type != nil {
|
|
386
|
+
return follow(d.Type)
|
|
387
|
+
}
|
|
388
|
+
for i, name := range d.Names {
|
|
389
|
+
if name.Name == v.Name && i < len(d.Values) {
|
|
390
|
+
return follow(d.Values[i])
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
case *ast.AssignStmt:
|
|
394
|
+
for i, lhs := range d.Lhs {
|
|
395
|
+
if id, ok := lhs.(*ast.Ident); ok && id.Name == v.Name && i < len(d.Rhs) {
|
|
396
|
+
return follow(d.Rhs[i])
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
case *ast.TypeSpec:
|
|
400
|
+
if _, ok := d.Type.(*ast.StructType); ok {
|
|
401
|
+
return src.pkg + "." + d.Name.Name
|
|
402
|
+
}
|
|
403
|
+
return follow(d.Type)
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
decl, owner := find(src.pkg, v.Name)
|
|
407
|
+
return receiverType(decl, owner, sources, seen)
|
|
408
|
+
}
|
|
409
|
+
case *ast.TypeSpec:
|
|
410
|
+
if _, ok := v.Type.(*ast.StructType); ok {
|
|
411
|
+
return src.pkg + "." + v.Name.Name
|
|
412
|
+
}
|
|
413
|
+
return follow(v.Type)
|
|
414
|
+
case *ast.FuncDecl:
|
|
415
|
+
if v.Type.Results != nil && len(v.Type.Results.List) > 0 {
|
|
416
|
+
return follow(v.Type.Results.List[0].Type)
|
|
417
|
+
}
|
|
418
|
+
case *ast.CallExpr:
|
|
419
|
+
if id, ok := v.Fun.(*ast.Ident); ok && id.Name == "new" && len(v.Args) == 1 {
|
|
420
|
+
return follow(v.Args[0])
|
|
421
|
+
}
|
|
422
|
+
pkg, name := src.pkg, ""
|
|
423
|
+
switch fun := v.Fun.(type) {
|
|
424
|
+
case *ast.Ident:
|
|
425
|
+
name = fun.Name
|
|
426
|
+
case *ast.SelectorExpr:
|
|
427
|
+
if id, ok := fun.X.(*ast.Ident); ok {
|
|
428
|
+
pkg, name = src.imports[id.Name], fun.Sel.Name
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
decl, owner := find(pkg, name)
|
|
432
|
+
return receiverType(decl, owner, sources, seen)
|
|
433
|
+
}
|
|
434
|
+
return ""
|
|
25
435
|
}
|
|
26
436
|
|
|
27
437
|
func main() {
|
|
@@ -30,7 +440,7 @@ func main() {
|
|
|
30
440
|
fmt.Fprintln(os.Stderr, err)
|
|
31
441
|
os.Exit(1)
|
|
32
442
|
}
|
|
33
|
-
out := result{Problems: []string{}}
|
|
443
|
+
out := result{Problems: []string{}, HTTPRoutes: []httpRoute{}}
|
|
34
444
|
sdk := map[string]bool{}
|
|
35
445
|
for _, group := range in.SDK {
|
|
36
446
|
for method := range group {
|
|
@@ -38,50 +448,55 @@ func main() {
|
|
|
38
448
|
}
|
|
39
449
|
}
|
|
40
450
|
set := token.NewFileSet()
|
|
41
|
-
err :=
|
|
451
|
+
files, err := shippedFiles(in)
|
|
452
|
+
if err != nil {
|
|
453
|
+
fmt.Fprintln(os.Stderr, err)
|
|
454
|
+
os.Exit(1)
|
|
455
|
+
}
|
|
456
|
+
var sources []source
|
|
457
|
+
for path, pkg := range files {
|
|
458
|
+
file, err := parser.ParseFile(set, path, nil, 0)
|
|
42
459
|
if err != nil {
|
|
43
|
-
|
|
460
|
+
fmt.Fprintln(os.Stderr, "invalid shipped Go source")
|
|
461
|
+
os.Exit(1)
|
|
44
462
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if d.Type()&os.ModeSymlink != 0 {
|
|
52
|
-
target, err := os.Stat(path)
|
|
53
|
-
if err == nil && target.IsDir() {
|
|
54
|
-
shown, _ := filepath.Rel(in.Dir, path)
|
|
55
|
-
out.Problems = append(out.Problems, filepath.ToSlash(shown)+":0: Go source directories cannot be symlinks")
|
|
56
|
-
return nil
|
|
463
|
+
imports := map[string]string{}
|
|
464
|
+
for _, spec := range file.Imports {
|
|
465
|
+
module, _ := strconv.Unquote(spec.Path.Value)
|
|
466
|
+
alias := filepath.Base(module)
|
|
467
|
+
if spec.Name != nil {
|
|
468
|
+
alias = spec.Name.Name
|
|
57
469
|
}
|
|
470
|
+
imports[alias] = module
|
|
58
471
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
472
|
+
sources = append(sources, source{file, path, pkg, imports})
|
|
473
|
+
}
|
|
474
|
+
safe := boundaryHandlers(sources)
|
|
475
|
+
for _, src := range sources {
|
|
476
|
+
path, file, imports := src.path, src.file, src.imports
|
|
62
477
|
shown, _ := filepath.Rel(in.Dir, path)
|
|
63
478
|
problem := func(pos token.Pos, message string) {
|
|
64
479
|
out.Problems = append(out.Problems, fmt.Sprintf("%s:%d: %s", filepath.ToSlash(shown), set.Position(pos).Line, message))
|
|
65
480
|
}
|
|
66
|
-
|
|
481
|
+
d, err := os.Lstat(path)
|
|
482
|
+
if err != nil || d.Mode()&os.ModeSymlink != 0 {
|
|
67
483
|
problem(token.NoPos, "Go source symlinks cannot be audited")
|
|
68
|
-
|
|
484
|
+
continue
|
|
69
485
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
486
|
+
for dir := filepath.Dir(path); dir != filepath.Dir(dir); dir = filepath.Dir(dir) {
|
|
487
|
+
info, err := os.Lstat(dir)
|
|
488
|
+
if err == nil && info.Mode()&os.ModeSymlink != 0 {
|
|
489
|
+
problem(token.NoPos, "Go source directories cannot be symlinks")
|
|
490
|
+
break
|
|
491
|
+
}
|
|
492
|
+
if dir == in.Dir {
|
|
493
|
+
break
|
|
494
|
+
}
|
|
73
495
|
}
|
|
74
|
-
imports := map[string]string{}
|
|
75
496
|
for _, spec := range file.Imports {
|
|
76
|
-
|
|
77
|
-
alias := filepath.Base(module)
|
|
78
|
-
if spec.Name != nil {
|
|
79
|
-
alias = spec.Name.Name
|
|
80
|
-
}
|
|
81
|
-
if alias == "." {
|
|
497
|
+
if spec.Name != nil && spec.Name.Name == "." {
|
|
82
498
|
problem(spec.Pos(), "dot imports cannot be audited")
|
|
83
499
|
}
|
|
84
|
-
imports[alias] = module
|
|
85
500
|
}
|
|
86
501
|
constants := map[string]string{}
|
|
87
502
|
direct := map[*ast.SelectorExpr]bool{}
|
|
@@ -108,25 +523,47 @@ func main() {
|
|
|
108
523
|
}
|
|
109
524
|
}
|
|
110
525
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
111
|
-
if fn, ok := n.(*ast.FuncDecl); ok && fn.Recv != nil && fn.Name.Name == "ServeHTTP" {
|
|
112
|
-
problem(fn.Pos(), "
|
|
526
|
+
if fn, ok := n.(*ast.FuncDecl); ok && fn.Recv != nil && fn.Name.Name == "ServeHTTP" && !safe[n] {
|
|
527
|
+
problem(fn.Pos(), "unregistered ServeHTTP; use api.HandleHTTP, api.HandleIntegration or ipc.ServeUnix")
|
|
113
528
|
}
|
|
114
529
|
selector, ok := n.(*ast.SelectorExpr)
|
|
115
530
|
if !ok {
|
|
116
531
|
return true
|
|
117
532
|
}
|
|
533
|
+
if selector.Sel.Name == "ServeHTTP" && !safe[n] {
|
|
534
|
+
problem(selector.Pos(), "unregistered ServeHTTP dispatch")
|
|
535
|
+
}
|
|
536
|
+
receiver := receiverType(selector.X, src, sources, map[ast.Node]bool{})
|
|
537
|
+
if receiver == "net.ListenConfig" && (selector.Sel.Name == "Listen" || selector.Sel.Name == "ListenPacket") ||
|
|
538
|
+
receiver == "net/http.Server" && (selector.Sel.Name == "Serve" || selector.Sel.Name == "ServeTLS" ||
|
|
539
|
+
selector.Sel.Name == "ListenAndServe" || selector.Sel.Name == "ListenAndServeTLS") {
|
|
540
|
+
problem(selector.Pos(), "custom listener outside SDK boundaries: "+selector.Sel.Name)
|
|
541
|
+
}
|
|
118
542
|
pkg, ok := selector.X.(*ast.Ident)
|
|
119
543
|
if !ok {
|
|
120
544
|
return true
|
|
121
545
|
}
|
|
122
546
|
module := imports[pkg.Name]
|
|
123
547
|
name := selector.Sel.Name
|
|
548
|
+
if module == "net" && (name == "Listen" || name == "ListenTCP" || name == "ListenUnix" ||
|
|
549
|
+
name == "ListenPacket" || name == "ListenUDP" || name == "ListenIP" || name == "ListenUnixgram" ||
|
|
550
|
+
name == "FileListener" || name == "FilePacketConn") ||
|
|
551
|
+
module == "crypto/tls" && name == "Listen" ||
|
|
552
|
+
(module == "syscall" || module == "golang.org/x/sys/unix") && (name == "Bind" || name == "Listen" || name == "Socket") {
|
|
553
|
+
problem(selector.Pos(), "custom listener outside SDK boundaries: "+name)
|
|
554
|
+
}
|
|
555
|
+
if module == sdkModule+"/ipc" && !direct[selector] {
|
|
556
|
+
switch name {
|
|
557
|
+
case "ServeUnix", "StartUnix", "ServePeerUnix", "DialUnix", "DialPeerUnix", "ReserveLoopback", "ProbeLoopback":
|
|
558
|
+
problem(selector.Pos(), "IPC constructor aliases cannot be audited")
|
|
559
|
+
}
|
|
560
|
+
}
|
|
124
561
|
if module == "github.com/synopackageland/syno-pkg-land-sdk/idgateway" && (name == "NewFront" || name == "Build" || name == "Run") {
|
|
125
562
|
problem(selector.Pos(), "gateway listeners must use identity_gateway in the manifest; upstream_auth_paths never exempts SDK RPC or custom HTTP handlers")
|
|
126
563
|
}
|
|
127
564
|
if module == "github.com/synopackageland/syno-pkg-land-sdk/api" {
|
|
128
565
|
switch name {
|
|
129
|
-
case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch":
|
|
566
|
+
case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch", "HandleHTTP", "HandleIntegration":
|
|
130
567
|
if !direct[selector] {
|
|
131
568
|
problem(selector.Pos(), "route constructor aliases cannot be audited; register methods directly")
|
|
132
569
|
}
|
|
@@ -134,8 +571,20 @@ func main() {
|
|
|
134
571
|
}
|
|
135
572
|
if module == "net/http" {
|
|
136
573
|
switch name {
|
|
137
|
-
case "Handle", "HandleFunc", "
|
|
574
|
+
case "Handle", "HandleFunc", "Serve", "ServeTLS", "ListenAndServe", "ListenAndServeTLS":
|
|
138
575
|
problem(selector.Pos(), "HTTP handler outside the SDK registry: "+name)
|
|
576
|
+
case "HandlerFunc", "NewServeMux", "ServeMux", "FileServer", "FileServerFS", "StripPrefix", "TimeoutHandler", "NotFoundHandler", "RedirectHandler":
|
|
577
|
+
if !safe[n] {
|
|
578
|
+
problem(selector.Pos(), "HTTP handler outside the SDK registry: "+name)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if module == "net/http/httputil" && (name == "ReverseProxy" || name == "NewSingleHostReverseProxy") && !safe[n] {
|
|
583
|
+
problem(selector.Pos(), "HTTP proxy outside the SDK registry")
|
|
584
|
+
}
|
|
585
|
+
if module == "net/http/cgi" && name == "Serve" {
|
|
586
|
+
if !direct[selector] {
|
|
587
|
+
problem(selector.Pos(), "CGI constructor aliases cannot be audited")
|
|
139
588
|
}
|
|
140
589
|
}
|
|
141
590
|
return true
|
|
@@ -149,11 +598,34 @@ func main() {
|
|
|
149
598
|
if !ok {
|
|
150
599
|
return true
|
|
151
600
|
}
|
|
601
|
+
// An in-process administrator call names one fixed route that
|
|
602
|
+
// already requires DSM administrators; the SDK verifies the account.
|
|
603
|
+
if selector.Sel.Name == "InvokeAsAdministrator" {
|
|
604
|
+
method := ""
|
|
605
|
+
if len(call.Args) == 4 {
|
|
606
|
+
switch arg := call.Args[2].(type) {
|
|
607
|
+
case *ast.BasicLit:
|
|
608
|
+
if arg.Kind == token.STRING {
|
|
609
|
+
method, _ = strconv.Unquote(arg.Value)
|
|
610
|
+
}
|
|
611
|
+
case *ast.Ident:
|
|
612
|
+
method = constants[arg.Name]
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if method == "" {
|
|
616
|
+
problem(call.Pos(), "dynamic InvokeAsAdministrator method cannot be audited; name a literal method")
|
|
617
|
+
} else if in.Permissions[method] != "administrators" {
|
|
618
|
+
problem(call.Pos(), "InvokeAsAdministrator requires a method declared administrators in permissions.json: "+method)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
152
621
|
pkg, ok := selector.X.(*ast.Ident)
|
|
153
622
|
if !ok {
|
|
154
623
|
return true
|
|
155
624
|
}
|
|
156
625
|
module := imports[pkg.Name]
|
|
626
|
+
if module == "net/http/cgi" && selector.Sel.Name == "Serve" && len(call.Args) > 0 && reusesBoundary(call.Args[0], src, sources, safe, map[ast.Node]bool{}) {
|
|
627
|
+
problem(call.Pos(), "private or raw HTTP handler reused outside its SDK boundary")
|
|
628
|
+
}
|
|
157
629
|
if module == "github.com/synopackageland/syno-pkg-land-sdk/dsm" && selector.Sel.Name == "NewEntryGate" {
|
|
158
630
|
out.EntryGate = true
|
|
159
631
|
}
|
|
@@ -161,13 +633,17 @@ func main() {
|
|
|
161
633
|
return true
|
|
162
634
|
}
|
|
163
635
|
switch selector.Sel.Name {
|
|
164
|
-
case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch":
|
|
636
|
+
case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch", "HandleHTTP", "HandleIntegration":
|
|
165
637
|
default:
|
|
166
638
|
return true
|
|
167
639
|
}
|
|
168
640
|
method := ""
|
|
169
|
-
|
|
170
|
-
|
|
641
|
+
index := 0
|
|
642
|
+
if selector.Sel.Name == "HandleHTTP" {
|
|
643
|
+
index = 1
|
|
644
|
+
}
|
|
645
|
+
if len(call.Args) > index {
|
|
646
|
+
switch arg := call.Args[index].(type) {
|
|
171
647
|
case *ast.BasicLit:
|
|
172
648
|
if arg.Kind == token.STRING {
|
|
173
649
|
method, _ = strconv.Unquote(arg.Value)
|
|
@@ -181,14 +657,42 @@ func main() {
|
|
|
181
657
|
} else if _, declared := in.Permissions[method]; !declared && !sdk[method] {
|
|
182
658
|
problem(call.Pos(), "registered method missing from permissions.json: "+method)
|
|
183
659
|
}
|
|
660
|
+
// Only HandleIntegration skips the DSM session; the permission
|
|
661
|
+
// value and the constructor must agree in both directions.
|
|
662
|
+
if (selector.Sel.Name == "HandleIntegration") != (in.Permissions[method] == "integration") {
|
|
663
|
+
problem(call.Pos(), "the integration permission requires api.HandleIntegration, and api.HandleIntegration requires the integration permission")
|
|
664
|
+
}
|
|
665
|
+
if selector.Sel.Name == "HandleIntegration" && method != "" {
|
|
666
|
+
out.HTTPRoutes = append(out.HTTPRoutes, httpRoute{"?method=" + method, method})
|
|
667
|
+
}
|
|
668
|
+
if selector.Sel.Name == "HandleHTTP" {
|
|
669
|
+
prefix := ""
|
|
670
|
+
if len(call.Args) > 0 {
|
|
671
|
+
prefix = stringLiteral(call.Args[0])
|
|
672
|
+
}
|
|
673
|
+
if !regexp.MustCompile(`^/([A-Za-z0-9_-]+/)+$`).MatchString(prefix) {
|
|
674
|
+
problem(call.Pos(), "HTTP prefix must be a fixed literal full-segment path ending in /")
|
|
675
|
+
}
|
|
676
|
+
for _, reserved := range []string{"/_sso/", "/_spl/", "/api/", "/api.cgi/", "/rpc/", "/webman/", "/cgi/"} {
|
|
677
|
+
if strings.HasPrefix(prefix, reserved) || strings.HasPrefix(reserved, prefix) {
|
|
678
|
+
problem(call.Pos(), "HTTP prefix overlaps a reserved path")
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if sdk[method] || in.Permissions[method] != "authenticated" && in.Permissions[method] != "administrators" {
|
|
682
|
+
problem(call.Pos(), "HTTP routes require an explicit App authenticated or administrators permission")
|
|
683
|
+
}
|
|
684
|
+
for _, other := range out.HTTPRoutes {
|
|
685
|
+
if strings.HasPrefix(prefix, other.Prefix) || strings.HasPrefix(other.Prefix, prefix) {
|
|
686
|
+
problem(call.Pos(), "HTTP prefixes must not overlap")
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
out.HTTPRoutes = append(out.HTTPRoutes, httpRoute{prefix, method})
|
|
690
|
+
}
|
|
184
691
|
return true
|
|
185
692
|
})
|
|
186
|
-
return nil
|
|
187
|
-
})
|
|
188
|
-
if err != nil {
|
|
189
|
-
fmt.Fprintln(os.Stderr, err)
|
|
190
|
-
os.Exit(1)
|
|
191
693
|
}
|
|
694
|
+
sort.Strings(out.Problems)
|
|
695
|
+
sort.Slice(out.HTTPRoutes, func(i, j int) bool { return out.HTTPRoutes[i].Prefix < out.HTTPRoutes[j].Prefix })
|
|
192
696
|
if err := json.NewEncoder(os.Stdout).Encode(out); err != nil {
|
|
193
697
|
os.Exit(1)
|
|
194
698
|
}
|