omakit 0.4.1 → 0.4.3
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/README.md +32 -92
- package/package.json +1 -1
- package/skills/omarchy-plugin-check/SKILL.md +59 -3
- package/skills/omarchy-plugin-submit/SKILL.md +2 -2
- package/skills/omarchy-plugin-validation-watch/SKILL.md +4 -4
- package/tools/inspect/contract.mjs +245 -0
- package/tools/inspect/functions.mjs +129 -0
- package/tools/inspect/hosts.mjs +141 -0
- package/tools/inspect/inspect.mjs +170 -0
- package/tools/inspect/patterns.mjs +326 -0
- package/tools/inspect/processes.mjs +298 -0
- package/tools/inspect/report.mjs +342 -0
- package/tools/inspect/text.mjs +496 -0
- package/tools/inspect/timers.mjs +65 -0
- package/tools/inspect/walk.mjs +102 -0
- package/tools/inspect/writes.mjs +194 -0
- package/tools/marketplace/README.md +25 -2
- package/tools/marketplace/cli.mjs +84 -13
- package/tools/marketplace/github.mjs +13 -2
- package/tools/marketplace/local-transport.mjs +16 -4
- package/tools/marketplace/measure-review-cost.mjs +39 -0
- package/tools/marketplace/measure-staleness.mjs +76 -0
- package/tools/marketplace/options.mjs +1 -0
- package/tools/marketplace/pin.mjs +18 -10
- package/tools/marketplace/report.mjs +8 -1
- package/tools/marketplace/review-cost.mjs +107 -0
- package/tools/marketplace/run-baseline.mjs +2 -1
- package/tools/marketplace/style.mjs +3 -0
- package/tools/marketplace/submit.mjs +18 -2
- package/tools/marketplace/usage.mjs +21 -0
- package/tools/marketplace/verify.mjs +15 -4
- package/tools/marketplace/watch.mjs +64 -17
- package/tools/weigh/audit.mjs +21 -3
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Function sites: every named function, QML handler, shell function and
|
|
2
|
+
// Python def, with its length in lines, its deepest nesting and its branch
|
|
3
|
+
// count. Size is the one thing a person asks about a tree that regular
|
|
4
|
+
// expressions can answer without a parser: where does the logic pile up.
|
|
5
|
+
// The numbers are counts over the text; the threshold they are compared
|
|
6
|
+
// with is measured over listed trees (M12), never chosen.
|
|
7
|
+
|
|
8
|
+
import { blankComments, closingBracket, lineOf } from "./text.mjs"
|
|
9
|
+
|
|
10
|
+
const JS_BRANCH = /\b(?:if|else if|for|while|do|switch|case|catch)\b|&&|\|\||\?[^.:]/g
|
|
11
|
+
const SHELL_OPEN = /^\s*(?:if|for|while|until|case|select)\b/
|
|
12
|
+
const SHELL_CLOSE = /^\s*(?:fi|done|esac)\b/
|
|
13
|
+
const SHELL_BRANCH = /\b(?:if|elif|for|while|until|case)\b|\|\||&&|^\s*[^)]*\)\s*(?!\s*$)/
|
|
14
|
+
const PY_BRANCH = /^\s*(?:if|elif|for|while|except|with)\b|\band\b|\bor\b/
|
|
15
|
+
|
|
16
|
+
/** Deepest brace nesting inside a body, relative to the body itself. */
|
|
17
|
+
function braceDepth(body) {
|
|
18
|
+
let depth = 0
|
|
19
|
+
let deepest = 0
|
|
20
|
+
let quote = null
|
|
21
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
22
|
+
const ch = body[i]
|
|
23
|
+
if (quote) {
|
|
24
|
+
if (ch === "\\") i += 1
|
|
25
|
+
else if (ch === quote) quote = null
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch
|
|
29
|
+
else if (ch === "{") {
|
|
30
|
+
depth += 1
|
|
31
|
+
if (depth > deepest) deepest = depth
|
|
32
|
+
} else if (ch === "}") depth -= 1
|
|
33
|
+
}
|
|
34
|
+
return deepest
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function jsFunctions(file) {
|
|
38
|
+
const text = blankComments(file.text)
|
|
39
|
+
const rows = []
|
|
40
|
+
// `function name(` and, in QML, `onSomething: {` handlers with a body block.
|
|
41
|
+
const pattern = /(?<![\w.])function\s+([A-Za-z_$][\w$]*)\s*\(|(?<![\w.])(on[A-Z]\w*)\s*:\s*(?=\{)/g
|
|
42
|
+
for (const match of text.matchAll(pattern)) {
|
|
43
|
+
const open = text.indexOf("{", match.index + match[0].length - (match[2] ? 0 : 0))
|
|
44
|
+
if (open < 0) continue
|
|
45
|
+
const end = closingBracket(text, open)
|
|
46
|
+
if (end < 0) continue
|
|
47
|
+
const body = text.slice(open + 1, end)
|
|
48
|
+
const start = lineOf(text, match.index)
|
|
49
|
+
rows.push({
|
|
50
|
+
file: file.path,
|
|
51
|
+
line: start,
|
|
52
|
+
name: match[1] || match[2],
|
|
53
|
+
kind: match[1] ? "function" : "handler",
|
|
54
|
+
lines: lineOf(text, end) - start + 1,
|
|
55
|
+
depth: braceDepth(body),
|
|
56
|
+
branches: (body.match(JS_BRANCH) || []).length,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
return rows
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function shellFunctions(file) {
|
|
63
|
+
const lines = file.text.split("\n")
|
|
64
|
+
const rows = []
|
|
65
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
66
|
+
const head = lines[index].match(/^\s*(?:function\s+)?([A-Za-z_][\w-]*)\s*\(\)\s*\{?\s*$|^\s*function\s+([A-Za-z_][\w-]*)\s*\{?\s*$/)
|
|
67
|
+
if (!head) continue
|
|
68
|
+
const name = head[1] || head[2]
|
|
69
|
+
// The body runs to the line that is only `}` at the function's own indent.
|
|
70
|
+
const indent = lines[index].match(/^\s*/)[0]
|
|
71
|
+
let end = index
|
|
72
|
+
let depth = 0
|
|
73
|
+
let deepest = 0
|
|
74
|
+
let branches = 0
|
|
75
|
+
for (let at = index + 1; at < lines.length; at += 1) {
|
|
76
|
+
const line = lines[at]
|
|
77
|
+
if (line.trim() === "}" && line.startsWith(indent) && line.match(/^\s*/)[0].length === indent.length) {
|
|
78
|
+
end = at
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
if (SHELL_OPEN.test(line)) {
|
|
82
|
+
depth += 1
|
|
83
|
+
if (depth > deepest) deepest = depth
|
|
84
|
+
}
|
|
85
|
+
if (SHELL_CLOSE.test(line)) depth -= 1
|
|
86
|
+
if (SHELL_BRANCH.test(line.split("#")[0])) branches += 1
|
|
87
|
+
end = at
|
|
88
|
+
}
|
|
89
|
+
rows.push({ file: file.path, line: index + 1, name, kind: "function", lines: end - index + 1, depth: deepest, branches })
|
|
90
|
+
index = end
|
|
91
|
+
}
|
|
92
|
+
return rows
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function pythonFunctions(file) {
|
|
96
|
+
const lines = file.text.split("\n")
|
|
97
|
+
const rows = []
|
|
98
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
99
|
+
const head = lines[index].match(/^(\s*)(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/)
|
|
100
|
+
if (!head) continue
|
|
101
|
+
const base = head[1].length
|
|
102
|
+
let end = index
|
|
103
|
+
let deepest = 0
|
|
104
|
+
let branches = 0
|
|
105
|
+
for (let at = index + 1; at < lines.length; at += 1) {
|
|
106
|
+
const line = lines[at]
|
|
107
|
+
if (!line.trim()) continue
|
|
108
|
+
const indent = line.match(/^\s*/)[0].length
|
|
109
|
+
if (indent <= base) break
|
|
110
|
+
const level = Math.floor((indent - base) / 4)
|
|
111
|
+
if (level > deepest) deepest = level
|
|
112
|
+
if (PY_BRANCH.test(line)) branches += 1
|
|
113
|
+
end = at
|
|
114
|
+
}
|
|
115
|
+
rows.push({ file: file.path, line: index + 1, name: head[2], kind: "function", lines: end - index + 1, depth: deepest, branches })
|
|
116
|
+
}
|
|
117
|
+
return rows
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {{ path: string, kind: string, text: string }} file
|
|
122
|
+
* @returns {Array<{ file, line, name, kind, lines, depth, branches }>}
|
|
123
|
+
*/
|
|
124
|
+
export function extractFunctions(file) {
|
|
125
|
+
if (file.kind === "qml" || file.kind === "js") return jsFunctions(file)
|
|
126
|
+
if (file.kind === "shell") return shellFunctions(file)
|
|
127
|
+
if (file.kind === "python") return pythonFunctions(file)
|
|
128
|
+
return []
|
|
129
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Host sites: every `http://` or `https://` literal in code or argv, with
|
|
2
|
+
// the host and scheme it names, the tool the literal reaches (the argv it
|
|
3
|
+
// sits in, or the call on its line), and the timeout and size-cap flags
|
|
4
|
+
// that argv shows. A host built from a variable is not a host: a literal
|
|
5
|
+
// whose host part holds `${...}` or stops at `://` is recorded as not
|
|
6
|
+
// resolvable and never guessed.
|
|
7
|
+
|
|
8
|
+
import { basename, blankComments, lineOf, shellSegments, shellWords } from "./text.mjs"
|
|
9
|
+
import { toolOf } from "./processes.mjs"
|
|
10
|
+
|
|
11
|
+
const URL = /https?:\/\/[^\s"'`)\]}>,;]*/g
|
|
12
|
+
const TIMEOUT_FLAGS = ["--max-time", "-m", "--connect-timeout", "--timeout", "-T"]
|
|
13
|
+
const SIZE_FLAGS = ["--max-filesize", "--quota", "-Q"]
|
|
14
|
+
|
|
15
|
+
/** Loopback, link-local and RFC 1918 literals, and the names that resolve there. */
|
|
16
|
+
export function privateAddress(host) {
|
|
17
|
+
const h = String(host).toLowerCase().replace(/^\[|\]$/g, "")
|
|
18
|
+
if (["localhost", "0.0.0.0", "::1", "::"].includes(h)) return true
|
|
19
|
+
const v4 = h.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)
|
|
20
|
+
if (!v4) return /^f[cd][0-9a-f]{2}:|^fe80:/.test(h)
|
|
21
|
+
const [a, b] = [Number(v4[1]), Number(v4[2])]
|
|
22
|
+
return a === 10 || a === 127 || (a === 192 && b === 168) || (a === 172 && b >= 16 && b <= 31) || (a === 169 && b === 254) || (a === 100 && b >= 64 && b <= 127)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The host of a URL literal, or null when the text shows an expression where the host would be. */
|
|
26
|
+
export function hostOf(url) {
|
|
27
|
+
const rest = url.replace(/^https?:\/\//, "")
|
|
28
|
+
const authority = rest.split(/[/?#]/)[0]
|
|
29
|
+
if (!authority || /\$\{|\$\(|["'`+]/.test(authority)) return null
|
|
30
|
+
const host = authority.includes("@") ? authority.slice(authority.lastIndexOf("@") + 1) : authority
|
|
31
|
+
const bare = host.startsWith("[") ? host.slice(0, host.indexOf("]") + 1) : host.split(":")[0]
|
|
32
|
+
return bare && /^[\w.\-[\]:]+$/.test(bare) ? bare : null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The flag with its value: `--max-time 5`, `--max-time=5`, `-m5`. */
|
|
36
|
+
function flagValue(words, names) {
|
|
37
|
+
for (const [index, word] of words.entries()) {
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
if (word === name) return `${name} ${words[index + 1] ?? ""}`.trim()
|
|
40
|
+
if (word.startsWith(`${name}=`)) return word
|
|
41
|
+
if (name.length === 2 && word.startsWith(name) && word.length > 2 && /\d/.test(word[2])) return word
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The words a URL is invoked with: a `-c` script's own words when the URL sits inside one, otherwise the argv. */
|
|
48
|
+
function wordsAround(process, url) {
|
|
49
|
+
const argv = process.argv
|
|
50
|
+
const at = argv.findIndex((word) => word.includes(url))
|
|
51
|
+
if (at < 0 || !process.shellWrapper) return argv
|
|
52
|
+
const script = toolOf(argv).index + 2
|
|
53
|
+
if (at !== script) return argv
|
|
54
|
+
// The URL is a word inside the script string: the words of its own segment.
|
|
55
|
+
for (const segment of shellSegments(argv[at])) {
|
|
56
|
+
if (segment.text.includes(url)) return shellWords(segment.text)
|
|
57
|
+
}
|
|
58
|
+
return shellWords(argv[at])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function caps(words, wholeArgv) {
|
|
62
|
+
const joined = wholeArgv.join(" ")
|
|
63
|
+
const timeout = flagValue(words, TIMEOUT_FLAGS)
|
|
64
|
+
|| (wholeArgv.some((word) => basename(word) === "timeout") ? `timeout ${wholeArgv[wholeArgv.findIndex((word) => basename(word) === "timeout") + 1] ?? ""}`.trim() : null)
|
|
65
|
+
const head = joined.match(/(?:^|[\s|/])head\s+(?:-\S+\s+)*-c\s+(\S+)/)
|
|
66
|
+
const size = flagValue(words, SIZE_FLAGS) || (head ? `head -c ${head[1]}` : null)
|
|
67
|
+
return {
|
|
68
|
+
timeout: { observed: Boolean(timeout), via: timeout },
|
|
69
|
+
sizeCap: { observed: Boolean(size), via: size },
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Shell and Python comments blanked, quotes respected, line count kept. */
|
|
74
|
+
function blankHashComments(text) {
|
|
75
|
+
return text.split("\n").map((line) => {
|
|
76
|
+
let quote = null
|
|
77
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
78
|
+
const ch = line[i]
|
|
79
|
+
if (quote) {
|
|
80
|
+
if (ch === "\\") i += 1
|
|
81
|
+
else if (ch === quote) quote = null
|
|
82
|
+
} else if (ch === '"' || ch === "'") quote = ch
|
|
83
|
+
else if (ch === "#" && (i === 0 || /\s/.test(line[i - 1]))) return line.slice(0, i)
|
|
84
|
+
}
|
|
85
|
+
return line
|
|
86
|
+
}).join("\n")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function callOnLine(line, text) {
|
|
90
|
+
if (/(?<![\w.])fetch\s*\(/.test(line)) return "fetch"
|
|
91
|
+
if (/XMLHttpRequest/.test(line) || /XMLHttpRequest/.test(text)) return "XMLHttpRequest"
|
|
92
|
+
const call = line.match(/([A-Za-z_][\w.]*)\s*\([^()]*https?:\/\//)
|
|
93
|
+
return call ? call[1] : null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {{ path: string, kind: string, text: string }} file
|
|
98
|
+
* @param {Array} processes the process rows of the same file
|
|
99
|
+
* @returns {{ hosts: Array, notResolvable: Array }}
|
|
100
|
+
*/
|
|
101
|
+
export function extractHosts(file, processes = []) {
|
|
102
|
+
const text = file.kind === "qml" || file.kind === "js" ? blankComments(file.text) : file.kind === "shell" || file.kind === "python" ? blankHashComments(file.text) : file.text
|
|
103
|
+
const hosts = []
|
|
104
|
+
const notResolvable = []
|
|
105
|
+
for (const match of text.matchAll(URL)) {
|
|
106
|
+
const url = match[0]
|
|
107
|
+
const line = lineOf(text, match.index)
|
|
108
|
+
const scheme = url.startsWith("https") ? "https" : "http"
|
|
109
|
+
const host = hostOf(url)
|
|
110
|
+
if (!host) {
|
|
111
|
+
notResolvable.push({ file: file.path, line, kind: "host", text: url })
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
const process = processes.find((row) => row.file === file.path && Array.isArray(row.argv) && row.argv.some((word) => word.includes(url)) && (row.declaredIn !== "shell" || row.line === line))
|
|
115
|
+
let tool = null
|
|
116
|
+
let words = []
|
|
117
|
+
let argv = []
|
|
118
|
+
if (process) {
|
|
119
|
+
argv = process.argv
|
|
120
|
+
words = wordsAround(process, url)
|
|
121
|
+
tool = toolOf(words).tool
|
|
122
|
+
if (tool) tool = basename(tool)
|
|
123
|
+
} else {
|
|
124
|
+
const lineText = text.split("\n")[line - 1] || ""
|
|
125
|
+
tool = callOnLine(lineText, text)
|
|
126
|
+
}
|
|
127
|
+
const { timeout, sizeCap } = caps(words, argv)
|
|
128
|
+
hosts.push({
|
|
129
|
+
host,
|
|
130
|
+
scheme,
|
|
131
|
+
file: file.path,
|
|
132
|
+
line,
|
|
133
|
+
tool,
|
|
134
|
+
timeout,
|
|
135
|
+
sizeCap,
|
|
136
|
+
flags: words.filter((word) => word.startsWith("-") && word !== "-"),
|
|
137
|
+
privateAddress: privateAddress(host),
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
return { hosts, notResolvable }
|
|
141
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// `omakit inspect <plugin-dir>`: what a plugin tree does, as observations.
|
|
2
|
+
// Resolve the subject the way `submit` does, read its installable tree at
|
|
3
|
+
// the commit, run the four extractors over every file inspect reads, run
|
|
4
|
+
// the marketplace's own baseline through `verify` for the capabilities,
|
|
5
|
+
// and build the document of docs/INSPECT.md. No verdict, and the one score
|
|
6
|
+
// a position among listed plugins, never a grade: every
|
|
7
|
+
// row is a fact the text shows, labelled observed, and the document ends
|
|
8
|
+
// with what the method cannot see.
|
|
9
|
+
//
|
|
10
|
+
// Runs nothing from the tree, resolves no host, opens no socket, writes
|
|
11
|
+
// nothing into the tree. The one thing it fetches is what `verify` fetches,
|
|
12
|
+
// which under the local transport is nothing.
|
|
13
|
+
|
|
14
|
+
import { join } from "node:path"
|
|
15
|
+
import { resolveSubject, SubjectError } from "../subject/resolve.mjs"
|
|
16
|
+
import { marketplaceBaselineSection } from "../marketplace/verify.mjs"
|
|
17
|
+
import { consequence } from "../marketplace/preflight.mjs"
|
|
18
|
+
import { requirePin } from "../marketplace/pin.mjs"
|
|
19
|
+
import { omakitCacheDir } from "../marketplace/paths.mjs"
|
|
20
|
+
import { walkSubject } from "./walk.mjs"
|
|
21
|
+
import { extractProcesses } from "./processes.mjs"
|
|
22
|
+
import { extractHosts } from "./hosts.mjs"
|
|
23
|
+
import { extractWrites } from "./writes.mjs"
|
|
24
|
+
import { extractTimers } from "./timers.mjs"
|
|
25
|
+
import { extractFunctions } from "./functions.mjs"
|
|
26
|
+
import { evaluatePatterns, overSize, PATTERNS, rankOf, SIZE, sizeScore } from "./patterns.mjs"
|
|
27
|
+
|
|
28
|
+
export const METHOD = "static extraction, regular expressions over qml and shell; observed, not executed"
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What regular expressions over QML and shell cannot see, printed at the
|
|
32
|
+
* end of every report and carried verbatim in the document. A tree that
|
|
33
|
+
* shows none of the facts is "observed nothing of this kind", never clean,
|
|
34
|
+
* because of this list.
|
|
35
|
+
*/
|
|
36
|
+
export const NOT_VISIBLE = Object.freeze([
|
|
37
|
+
"commands built at run time",
|
|
38
|
+
"hosts and paths from variables, properties, config or the environment",
|
|
39
|
+
"scripts a command calls that inspect does not follow",
|
|
40
|
+
"components loaded from outside the tree",
|
|
41
|
+
"encoded or obfuscated content, and what a sh -c or eval string runs",
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
/** The failure codes that mean the target could not be read at all: exit 2, the contract's second status. */
|
|
45
|
+
export const NOT_READABLE = Object.freeze(["subject-not-found", "not-a-git-repository", "commit-not-found", "nothing-to-inspect", "usage"])
|
|
46
|
+
|
|
47
|
+
export class InspectError extends Error {
|
|
48
|
+
constructor(code, message, remedy = null) {
|
|
49
|
+
super(message)
|
|
50
|
+
this.name = "InspectError"
|
|
51
|
+
this.code = code
|
|
52
|
+
this.remedy = remedy
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {{ repoRoot: string, target: string, offline?: boolean, allowDirty?: boolean, omakitVersion: string, onPhase?: (text: string) => void, cacheRoot?: string }} options
|
|
58
|
+
* @returns {Promise<object>} the document of docs/INSPECT.md
|
|
59
|
+
*/
|
|
60
|
+
export async function inspectPlugin({ repoRoot, target, offline = false, allowDirty = false, omakitVersion, onPhase = () => {}, cacheRoot = omakitCacheDir() }) {
|
|
61
|
+
let subject
|
|
62
|
+
try {
|
|
63
|
+
subject = resolveSubject(target, { cacheRoot, allowDirty })
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof SubjectError) throw new InspectError(error.code, error.message)
|
|
66
|
+
throw error
|
|
67
|
+
}
|
|
68
|
+
onPhase("reading the installable tree")
|
|
69
|
+
const tree = walkSubject(subject)
|
|
70
|
+
const dir = subject.subdir ? join(subject.dir, subject.subdir) : subject.dir
|
|
71
|
+
if (!tree.manifest && !tree.manifestError) {
|
|
72
|
+
throw new InspectError("nothing-to-inspect", `no manifest.json at the root of ${dir}, so there is no plugin to inspect`, "Pass the directory that holds the plugin's manifest.json.")
|
|
73
|
+
}
|
|
74
|
+
const pluginId = typeof tree.manifest?.id === "string" ? tree.manifest.id.trim() : null
|
|
75
|
+
|
|
76
|
+
onPhase("reading processes, hosts, writes and timers")
|
|
77
|
+
const processes = []
|
|
78
|
+
const hosts = []
|
|
79
|
+
const writes = []
|
|
80
|
+
const timers = []
|
|
81
|
+
const functions = []
|
|
82
|
+
const notResolvable = []
|
|
83
|
+
for (const file of tree.files) {
|
|
84
|
+
// Each function carries its rank among the listed ones (M12).
|
|
85
|
+
functions.push(...extractFunctions(file).map((entry) => ({ ...entry, percentile: rankOf(entry) })))
|
|
86
|
+
const rows = extractProcesses(file)
|
|
87
|
+
processes.push(...rows)
|
|
88
|
+
for (const row of rows) {
|
|
89
|
+
if (row.argvForm === "computed") notResolvable.push({ file: row.file, line: row.line, kind: "command", text: `command: ${row.commandText}` })
|
|
90
|
+
}
|
|
91
|
+
const found = extractHosts(file, rows)
|
|
92
|
+
hosts.push(...found.hosts)
|
|
93
|
+
notResolvable.push(...found.notResolvable)
|
|
94
|
+
writes.push(...extractWrites(file, { pluginId }))
|
|
95
|
+
const ticking = extractTimers(file)
|
|
96
|
+
timers.push(...ticking.timers)
|
|
97
|
+
notResolvable.push(...ticking.notResolvable)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let marketplaceBaseline
|
|
101
|
+
let blockingRules = []
|
|
102
|
+
if (offline) {
|
|
103
|
+
marketplaceBaseline = { skipped: true, reason: "--offline" }
|
|
104
|
+
} else {
|
|
105
|
+
onPhase("running the official security baseline over a local snapshot")
|
|
106
|
+
// The baseline sees the plugin's tree and nothing around it: for a plugin
|
|
107
|
+
// kept below the root of a larger repository, the local transport serves
|
|
108
|
+
// that directory's tree as the whole tree. Without this the baseline
|
|
109
|
+
// scanned the repository root and reported the root's evidence as the
|
|
110
|
+
// plugin's, which is the 0.1 Passport's first failure.
|
|
111
|
+
marketplaceBaseline = await marketplaceBaselineSection({ repoRoot, subject, subdir: subject.subdir })
|
|
112
|
+
if (marketplaceBaseline.invoked && marketplaceBaseline.official && !marketplaceBaseline.official.error) {
|
|
113
|
+
blockingRules = (await consequence(requirePin(repoRoot).dir, marketplaceBaseline.official)).selectivelyBlockingRules
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const facts = { processes, hosts, writes, timers, notResolvable, files: tree.files, readme: tree.readme, baseline: marketplaceBaseline, blockingRules }
|
|
118
|
+
const { patterns, lookedFor } = evaluatePatterns(facts)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
omakit: omakitVersion,
|
|
122
|
+
command: "inspect",
|
|
123
|
+
method: METHOD,
|
|
124
|
+
subject: {
|
|
125
|
+
dir,
|
|
126
|
+
commit: subject.commit,
|
|
127
|
+
repository: { url: subject.repository.url },
|
|
128
|
+
mode: subject.mode,
|
|
129
|
+
pluginId,
|
|
130
|
+
filesRead: tree.filesRead,
|
|
131
|
+
},
|
|
132
|
+
observed: { processes, hosts, writes, timers, functions },
|
|
133
|
+
// Size: the functions over the M12 thresholds, longest first. A count of
|
|
134
|
+
// lines, branches and nesting over the text, compared with what 90 of
|
|
135
|
+
// 100 functions in listed trees stay under; never a judgement.
|
|
136
|
+
size: {
|
|
137
|
+
measurement: SIZE.measurement,
|
|
138
|
+
sample: { trees: SIZE.trees, functions: SIZE.functions },
|
|
139
|
+
thresholds: { lines: SIZE.lines, branches: SIZE.branches, depth: SIZE.depth },
|
|
140
|
+
// 10 minus the mean rank of this tree's functions among the listed
|
|
141
|
+
// ones: where the tree sits, never whether it is good; null with no
|
|
142
|
+
// function to rank.
|
|
143
|
+
score: sizeScore(functions),
|
|
144
|
+
over: overSize(functions),
|
|
145
|
+
},
|
|
146
|
+
// The headline split: a shell script contributes one site per command
|
|
147
|
+
// segment, so a tree with a few scripts carries hundreds of process
|
|
148
|
+
// sites beside a handful of QML Process blocks, and the two are said
|
|
149
|
+
// apart wherever the count is printed.
|
|
150
|
+
counts: {
|
|
151
|
+
processes: {
|
|
152
|
+
total: processes.length,
|
|
153
|
+
qml: processes.filter((row) => row.declaredIn === "qml").length,
|
|
154
|
+
shell: processes.filter((row) => row.declaredIn === "shell").length,
|
|
155
|
+
},
|
|
156
|
+
hosts: hosts.length,
|
|
157
|
+
writes: writes.length,
|
|
158
|
+
timers: timers.length,
|
|
159
|
+
functions: functions.length,
|
|
160
|
+
notResolvable: notResolvable.length,
|
|
161
|
+
},
|
|
162
|
+
notResolvable,
|
|
163
|
+
patterns,
|
|
164
|
+
lookedFor,
|
|
165
|
+
notVisible: [...NOT_VISIBLE],
|
|
166
|
+
marketplaceBaseline,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export { PATTERNS }
|