agent-security-lens 0.1.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.
Files changed (81) hide show
  1. package/.env.example +10 -0
  2. package/.mcp/server.json +42 -0
  3. package/CHANGELOG.md +17 -0
  4. package/LICENSE +17 -0
  5. package/PRIVACY.md +37 -0
  6. package/README.md +150 -0
  7. package/RELEASE-MANIFEST.json +449 -0
  8. package/SECURITY.md +24 -0
  9. package/apps/mcp-server/agent-security-lens-mcp.mjs +441 -0
  10. package/bin/agent-security-lens.mjs +117 -0
  11. package/data/ecosystems/agent-candidates.json +230 -0
  12. package/data/intelligence/components.json +22989 -0
  13. package/data/intelligence/security-evaluation-standard.json +221 -0
  14. package/data/recommendations/core/recommendations.json +256 -0
  15. package/data/trust/signal-taxonomy.json +107 -0
  16. package/docs/asl-agent-component-safety-standard-v0.2.md +56 -0
  17. package/examples/dot-hermes/.hermes/config.json +17 -0
  18. package/examples/dot-openclaw/.openclaw/openclaw.json +17 -0
  19. package/examples/hermes-like/.env.example +2 -0
  20. package/examples/hermes-like/config.json +37 -0
  21. package/examples/hermes-like/optional-mcps/github-tools.json +8 -0
  22. package/examples/hermes-like/skills/openclaw-imports/browser-skill/SKILL.md +8 -0
  23. package/examples/openclaw-like/.env.example +2 -0
  24. package/examples/openclaw-like/AGENTS.md +7 -0
  25. package/examples/openclaw-like/openclaw.json +28 -0
  26. package/examples/openclaw-like/workspace/skills/browser-control/SKILL.md +8 -0
  27. package/llms.txt +25 -0
  28. package/package.json +50 -0
  29. package/profiles/generic-agent/profile.json +19 -0
  30. package/profiles/hermes-like/profile.json +23 -0
  31. package/profiles/mcp-server/profile.json +18 -0
  32. package/profiles/openclaw-like/profile.json +22 -0
  33. package/profiles/skill-runtime/profile.json +19 -0
  34. package/rule-packs/core/rules.json +82 -0
  35. package/rule-packs/hermes/rules.json +44 -0
  36. package/rule-packs/mcp/rules.json +65 -0
  37. package/rule-packs/openclaw/rules.json +46 -0
  38. package/rule-packs/skills/rules.json +45 -0
  39. package/schemas/agent-install-decision.schema.json +432 -0
  40. package/schemas/agent-usage-event.schema.json +45 -0
  41. package/schemas/assessment-result.schema.json +361 -0
  42. package/schemas/comparison-result.schema.json +113 -0
  43. package/schemas/component-alternative-graph.schema.json +187 -0
  44. package/schemas/component-intelligence.schema.json +93 -0
  45. package/schemas/decision-feedback.schema.json +49 -0
  46. package/schemas/ecosystem-candidate-registry.schema.json +98 -0
  47. package/schemas/profile.schema.json +65 -0
  48. package/schemas/recommendation-pack.schema.json +114 -0
  49. package/schemas/rule-pack.schema.json +113 -0
  50. package/schemas/trust-signal-taxonomy.schema.json +68 -0
  51. package/scripts/verify-examples.mjs +121 -0
  52. package/scripts/verify-mcp-server.mjs +278 -0
  53. package/scripts/verify-registry.mjs +264 -0
  54. package/server.json +42 -0
  55. package/src/assessment/assess.mjs +108 -0
  56. package/src/assessment/discover-targets.mjs +127 -0
  57. package/src/assessment/risk-domains.mjs +83 -0
  58. package/src/assessment/summarize.mjs +57 -0
  59. package/src/core/files.mjs +74 -0
  60. package/src/intelligence/cloud-client.mjs +260 -0
  61. package/src/intelligence/component-intelligence.mjs +358 -0
  62. package/src/intelligence/decision-engine.mjs +772 -0
  63. package/src/intelligence/finding-context.mjs +180 -0
  64. package/src/intelligence/safety-score-v0.2.mjs +294 -0
  65. package/src/observations/json-observations.mjs +211 -0
  66. package/src/observations/observation-rules.mjs +157 -0
  67. package/src/profiles/load-profiles.mjs +130 -0
  68. package/src/recommendations/component-alternative-graph.mjs +94 -0
  69. package/src/recommendations/load-recommendations.mjs +17 -0
  70. package/src/recommendations/match-recommendations.mjs +79 -0
  71. package/src/report/comparison-console.mjs +71 -0
  72. package/src/report/console.mjs +103 -0
  73. package/src/report/markdown.mjs +145 -0
  74. package/src/results/compare-results.mjs +106 -0
  75. package/src/results/save-result.mjs +29 -0
  76. package/src/rules/load-rules.mjs +22 -0
  77. package/src/rules/match-rules.mjs +99 -0
  78. package/src/rules/supersedes.mjs +39 -0
  79. package/src/store/assessment-store.mjs +78 -0
  80. package/src/trust/derive-trust-signals.mjs +73 -0
  81. package/src/trust/load-trust-signals.mjs +17 -0
package/server.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.professor2k8/agent-security-lens",
4
+ "title": "AgentSecurityLens",
5
+ "description": "Security intelligence MCP for agents to review MCPs, Skills and tools before installation.",
6
+ "status": "active",
7
+ "repository": {
8
+ "url": "https://github.com/professor2k8/agent-security-lens",
9
+ "source": "github"
10
+ },
11
+ "version": "0.1.0",
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "identifier": "agent-security-lens",
16
+ "version": "0.1.0",
17
+ "transport": {
18
+ "type": "stdio"
19
+ },
20
+ "environmentVariables": [
21
+ {
22
+ "name": "ASL_API_URL",
23
+ "description": "AgentSecurityLens Cloud Intelligence API URL.",
24
+ "isRequired": false,
25
+ "default": "https://api.agentsecuritylens.com"
26
+ },
27
+ {
28
+ "name": "ASL_API_KEY",
29
+ "description": "Optional API key for Team, Pro or Enterprise use.",
30
+ "isRequired": false,
31
+ "isSecret": true
32
+ },
33
+ {
34
+ "name": "ASL_MODE",
35
+ "description": "Set to local for offline fallback mode.",
36
+ "isRequired": false,
37
+ "default": "online"
38
+ }
39
+ ]
40
+ }
41
+ ]
42
+ }
@@ -0,0 +1,108 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { discoverFiles } from "../core/files.mjs";
5
+ import { extractJsonObservations } from "../observations/json-observations.mjs";
6
+ import { findingsFromObservations } from "../observations/observation-rules.mjs";
7
+ import { loadProfiles, resolveProfileSelection } from "../profiles/load-profiles.mjs";
8
+ import { loadRecommendationPacks } from "../recommendations/load-recommendations.mjs";
9
+ import { applyRecommendations } from "../recommendations/match-recommendations.mjs";
10
+ import { loadRulePacks } from "../rules/load-rules.mjs";
11
+ import { matchRules } from "../rules/match-rules.mjs";
12
+ import { applySupersedes } from "../rules/supersedes.mjs";
13
+ import { deriveTrustSignals, summarizeTrustSignals } from "../trust/derive-trust-signals.mjs";
14
+ import { loadTrustSignalTaxonomies } from "../trust/load-trust-signals.mjs";
15
+ import { groupByRiskDomain } from "./risk-domains.mjs";
16
+ import { summarize } from "./summarize.mjs";
17
+
18
+ export async function assess({ targetPath, requestedProfile }) {
19
+ const startedAt = new Date().toISOString();
20
+ const root = resolve(targetPath);
21
+ const profiles = await loadProfiles();
22
+ const profileSelection = await resolveProfileSelection({ profiles, requestedProfile, root });
23
+ const matchedProfiles = profileSelection.profiles;
24
+ const rulePacks = await loadRulePacks(matchedProfiles);
25
+ const recommendationPacks = await loadRecommendationPacks();
26
+ const trustSignalTaxonomies = await loadTrustSignalTaxonomies();
27
+ const files = await discoverFiles(root);
28
+ const textFindings = await matchRules({ root, files, profiles: matchedProfiles, rulePacks });
29
+ const observations = await extractJsonObservations(files);
30
+ const observationFindings = findingsFromObservations({
31
+ observations,
32
+ profileIds: matchedProfiles.map((profile) => profile.id)
33
+ });
34
+ const findings = applyRecommendations(
35
+ applySupersedes([...observationFindings, ...textFindings]),
36
+ recommendationPacks
37
+ );
38
+ const trustSignals = deriveTrustSignals({ findings, taxonomies: trustSignalTaxonomies });
39
+ const summary = summarize(findings, matchedProfiles);
40
+ summary.trust_signal_summary = summarizeTrustSignals(trustSignals);
41
+ const riskDomains = groupByRiskDomain(findings);
42
+ const completedAt = new Date().toISOString();
43
+ const assessmentId = createHash("sha256")
44
+ .update([root, startedAt, completedAt, profileSelection.selected_profile].join("|"))
45
+ .digest("hex")
46
+ .slice(0, 16);
47
+
48
+ return {
49
+ schema_version: "0.1.0",
50
+ tool: {
51
+ name: "AgentSecurityLens",
52
+ version: "0.1.0-alpha.0"
53
+ },
54
+ assessment: {
55
+ id: assessmentId,
56
+ target_path: root,
57
+ target_url: pathToFileURL(root).href,
58
+ started_at: startedAt,
59
+ completed_at: completedAt
60
+ },
61
+ lineage: {
62
+ profile_selection: {
63
+ mode: profileSelection.mode,
64
+ requested_profile: profileSelection.requested_profile,
65
+ selected_profile: profileSelection.selected_profile,
66
+ detection_signals: profileSelection.detection_signals
67
+ },
68
+ algorithms: [
69
+ { id: "json-observation-extractor", version: "0.1.0" },
70
+ { id: "observation-to-finding-rules", version: "0.1.0" },
71
+ { id: "finding-supersedes", version: "0.1.0" },
72
+ { id: "recommendation-matcher", version: "0.1.0" },
73
+ { id: "trust-signal-deriver", version: "0.1.0" },
74
+ { id: summary.scoring_model, version: "0.1.0" }
75
+ ]
76
+ },
77
+ profiles: matchedProfiles.map((profile) => ({
78
+ id: profile.id,
79
+ version: profile.version,
80
+ status: profile.status,
81
+ confidence: profile.confidence,
82
+ coverage: profile.coverage,
83
+ known_limitations: profile.known_limitations
84
+ })),
85
+ rule_packs: rulePacks.map((pack) => ({
86
+ id: pack.id,
87
+ version: pack.version,
88
+ rule_count: pack.rules.length
89
+ })),
90
+ recommendation_packs: recommendationPacks.map((pack) => ({
91
+ id: pack.id,
92
+ version: pack.version,
93
+ status: pack.status,
94
+ recommendation_count: pack.recommendations.length
95
+ })),
96
+ trust_signal_taxonomies: trustSignalTaxonomies.map((taxonomy) => ({
97
+ id: taxonomy.id,
98
+ version: taxonomy.version,
99
+ status: taxonomy.status,
100
+ signal_count: taxonomy.signals.length
101
+ })),
102
+ trust_signals: trustSignals,
103
+ observations,
104
+ summary,
105
+ risk_domains: riskDomains,
106
+ findings
107
+ };
108
+ }
@@ -0,0 +1,127 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join, relative, resolve } from "node:path";
3
+
4
+ const IGNORED_DIRS = new Set([
5
+ ".git",
6
+ ".agentsecuritylens",
7
+ ".agentsecuritylens-test",
8
+ "node_modules",
9
+ "dist",
10
+ "build",
11
+ ".next",
12
+ ".cache",
13
+ "__pycache__"
14
+ ]);
15
+
16
+ const MARKERS = [
17
+ { profile: "openclaw-like", file: "openclaw.json", reason: "OpenClaw config" },
18
+ { profile: "openclaw-like", file: ".openclaw/openclaw.json", reason: "OpenClaw local config" },
19
+ { profile: "openclaw-like", file: "SOUL.md", reason: "OpenClaw instruction file" },
20
+ { profile: "openclaw-like", file: "TOOLS.md", reason: "OpenClaw tool manifest" },
21
+ { profile: "openclaw-like", file: "workspace/skills", reason: "OpenClaw skills workspace" },
22
+ { profile: "hermes-like", file: ".hermes", reason: "Hermes config directory" },
23
+ { profile: "hermes-like", file: ".hermes/config.json", reason: "Hermes JSON config" },
24
+ { profile: "hermes-like", file: ".hermes/config.yaml", reason: "Hermes YAML config" },
25
+ { profile: "hermes-like", file: "optional-mcps", reason: "Hermes optional MCPs" },
26
+ { profile: "hermes-like", file: "optional-skills", reason: "Hermes optional skills" },
27
+ { profile: "hermes-like", file: "gateway", reason: "Hermes gateway" },
28
+ { profile: "hermes-like", file: "cron", reason: "Hermes scheduled runs" },
29
+ { profile: "skill-runtime", file: "SKILL.md", reason: "Agent skill definition" },
30
+ { profile: "mcp-server", file: "mcp.json", reason: "MCP manifest" }
31
+ ];
32
+
33
+ async function listEntries(dir) {
34
+ try {
35
+ return await readdir(dir, { withFileTypes: true });
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+
41
+ async function hasPath(root, path) {
42
+ const parts = path.split("/");
43
+ let entries = await listEntries(root);
44
+ for (let index = 0; index < parts.length; index += 1) {
45
+ const part = parts[index];
46
+ const entry = entries.find((item) => item.name.toLowerCase() === part.toLowerCase());
47
+ if (!entry) return false;
48
+ if (index === parts.length - 1) return true;
49
+ if (!entry.isDirectory()) return false;
50
+ entries = await listEntries(join(root, entry.name));
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function parentTargetForMarker(dir, markerFile) {
56
+ if (markerFile === "SKILL.md") return dir;
57
+ return dir;
58
+ }
59
+
60
+ function confidenceFor(signals) {
61
+ if (signals.length >= 3) return 0.9;
62
+ if (signals.length === 2) return 0.78;
63
+ return 0.62;
64
+ }
65
+
66
+ function labelFor(path, workspaceRoot) {
67
+ const rel = relative(workspaceRoot, path).replaceAll("\\", "/");
68
+ return rel || ".";
69
+ }
70
+
71
+ export async function discoverTargets({ workspacePath, maxDepth = 4 }) {
72
+ const workspaceRoot = resolve(workspacePath);
73
+ const candidates = new Map();
74
+
75
+ async function inspectDir(dir, depth) {
76
+ const matched = [];
77
+ for (const marker of MARKERS) {
78
+ if (await hasPath(dir, marker.file)) {
79
+ matched.push(marker);
80
+ }
81
+ }
82
+
83
+ if (matched.length) {
84
+ const targetPath = parentTargetForMarker(dir, matched[0].file);
85
+ const existing = candidates.get(targetPath) || {
86
+ path: targetPath,
87
+ label: labelFor(targetPath, workspaceRoot),
88
+ profile: matched[0].profile,
89
+ signals: []
90
+ };
91
+ for (const marker of matched) {
92
+ existing.signals.push({
93
+ profile: marker.profile,
94
+ marker: marker.file,
95
+ reason: marker.reason
96
+ });
97
+ }
98
+ const byProfile = new Map();
99
+ for (const signal of existing.signals) {
100
+ byProfile.set(signal.profile, (byProfile.get(signal.profile) || 0) + 1);
101
+ }
102
+ existing.profile = [...byProfile.entries()].sort((a, b) => b[1] - a[1])[0][0];
103
+ existing.confidence = confidenceFor(existing.signals);
104
+ candidates.set(targetPath, existing);
105
+ }
106
+
107
+ if (depth >= maxDepth) return;
108
+ for (const entry of await listEntries(dir)) {
109
+ if (!entry.isDirectory() || IGNORED_DIRS.has(entry.name)) continue;
110
+ await inspectDir(join(dir, entry.name), depth + 1);
111
+ }
112
+ }
113
+
114
+ await inspectDir(workspaceRoot, 0);
115
+ const targets = [...candidates.values()].sort((a, b) => a.label.localeCompare(b.label));
116
+ const deduped = targets.filter((target) => {
117
+ return !targets.some((other) => {
118
+ if (other === target || other.profile !== target.profile) return false;
119
+ const rel = relative(other.path, target.path);
120
+ return rel && !rel.startsWith("..") && rel !== "." && !rel.includes(":");
121
+ });
122
+ });
123
+ return {
124
+ workspace_path: workspaceRoot,
125
+ targets: deduped
126
+ };
127
+ }
@@ -0,0 +1,83 @@
1
+ const DOMAIN_DEFINITIONS = [
2
+ {
3
+ id: "mcp",
4
+ title: "MCP",
5
+ match: (finding) => finding.permissions.includes("mcp-tool-access") || finding.rule_id.includes("mcp")
6
+ },
7
+ {
8
+ id: "skills",
9
+ title: "Skills",
10
+ match: (finding) => finding.permissions.includes("skill-installation") || finding.rule_id.includes("skill")
11
+ },
12
+ {
13
+ id: "remote-triggers",
14
+ title: "Remote Triggers",
15
+ match: (finding) => finding.permissions.includes("remote-trigger")
16
+ },
17
+ {
18
+ id: "scheduler",
19
+ title: "Scheduler",
20
+ match: (finding) => finding.permissions.includes("scheduled-execution")
21
+ },
22
+ {
23
+ id: "credentials",
24
+ title: "Credentials",
25
+ match: (finding) => finding.permissions.includes("credential-access") || finding.permissions.includes("env-read")
26
+ },
27
+ {
28
+ id: "execution",
29
+ title: "Execution",
30
+ match: (finding) => finding.category === "execution-risk"
31
+ },
32
+ {
33
+ id: "supply-chain",
34
+ title: "Supply Chain",
35
+ match: (finding) => finding.category === "supply-chain-risk"
36
+ }
37
+ ];
38
+
39
+ const severityRank = {
40
+ critical: 5,
41
+ high: 4,
42
+ medium: 3,
43
+ low: 2,
44
+ info: 1
45
+ };
46
+
47
+ function highestSeverity(findings) {
48
+ return findings.reduce((highest, finding) => {
49
+ if (!highest) return finding.severity;
50
+ return severityRank[finding.severity] > severityRank[highest] ? finding.severity : highest;
51
+ }, null);
52
+ }
53
+
54
+ export function groupByRiskDomain(findings) {
55
+ const domains = [];
56
+ const assigned = new Set();
57
+
58
+ for (const definition of DOMAIN_DEFINITIONS) {
59
+ const domainFindings = findings.filter((finding) => definition.match(finding));
60
+ if (!domainFindings.length) continue;
61
+ for (const finding of domainFindings) assigned.add(finding.id);
62
+ domains.push({
63
+ id: definition.id,
64
+ title: definition.title,
65
+ count: domainFindings.length,
66
+ highest_severity: highestSeverity(domainFindings),
67
+ findings: domainFindings.map((finding) => finding.id)
68
+ });
69
+ }
70
+
71
+ const otherFindings = findings.filter((finding) => !assigned.has(finding.id));
72
+ if (otherFindings.length) {
73
+ domains.push({
74
+ id: "other",
75
+ title: "Other",
76
+ count: otherFindings.length,
77
+ highest_severity: highestSeverity(otherFindings),
78
+ findings: otherFindings.map((finding) => finding.id)
79
+ });
80
+ }
81
+
82
+ return domains;
83
+ }
@@ -0,0 +1,57 @@
1
+ const severityWeights = {
2
+ critical: 22,
3
+ high: 12,
4
+ medium: 5,
5
+ low: 1,
6
+ info: 0
7
+ };
8
+
9
+ function categoryMultiplier(category) {
10
+ if (category === "execution-risk") return 1.15;
11
+ if (category === "data-exposure-risk") return 1.1;
12
+ if (category === "supply-chain-risk") return 1.05;
13
+ return 1;
14
+ }
15
+
16
+ export function summarize(findings, profiles = []) {
17
+ const bySeverity = {};
18
+ const byCategory = {};
19
+
20
+ for (const finding of findings) {
21
+ bySeverity[finding.severity] = (bySeverity[finding.severity] || 0) + 1;
22
+ byCategory[finding.category] = (byCategory[finding.category] || 0) + 1;
23
+ }
24
+
25
+ const rawPenalty = findings.reduce((total, finding) => {
26
+ const base = severityWeights[finding.severity] || 0;
27
+ const confidence = typeof finding.confidence === "number" ? finding.confidence : 0.7;
28
+ return total + base * confidence * categoryMultiplier(finding.category);
29
+ }, 0);
30
+
31
+ const profileCoverageAverage = profiles.length
32
+ ? profiles.reduce((total, profile) => total + (profile.coverage || 0), 0) / profiles.length
33
+ : 0.5;
34
+ const lowCoveragePenalty = profileCoverageAverage < 0.5 ? 6 : 0;
35
+ const penalty = Math.round(rawPenalty + lowCoveragePenalty);
36
+
37
+ const topFindings = findings
38
+ .slice()
39
+ .sort((a, b) => (severityWeights[b.severity] || 0) - (severityWeights[a.severity] || 0))
40
+ .slice(0, 5)
41
+ .map((finding) => ({
42
+ title: finding.title,
43
+ severity: finding.severity,
44
+ category: finding.category,
45
+ evidence: finding.evidence
46
+ }));
47
+
48
+ return {
49
+ total_findings: findings.length,
50
+ by_severity: bySeverity,
51
+ by_category: byCategory,
52
+ trust_score: Math.max(0, 100 - penalty),
53
+ scoring_model: "simple-penalty@0.1.0",
54
+ profile_coverage_average: Number(profileCoverageAverage.toFixed(2)),
55
+ top_findings: topFindings
56
+ };
57
+ }
@@ -0,0 +1,74 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+
4
+ const DEFAULT_IGNORES = new Set([
5
+ ".git",
6
+ "node_modules",
7
+ "dist",
8
+ "build",
9
+ ".next",
10
+ ".cache",
11
+ "__pycache__"
12
+ ]);
13
+
14
+ const MAX_FILE_SIZE_BYTES = 512 * 1024;
15
+ const SUPPORTED_EXTENSIONS = new Set([
16
+ "",
17
+ ".md",
18
+ ".txt",
19
+ ".json",
20
+ ".jsonc",
21
+ ".yaml",
22
+ ".yml",
23
+ ".toml",
24
+ ".js",
25
+ ".mjs",
26
+ ".cjs",
27
+ ".ts",
28
+ ".tsx",
29
+ ".py",
30
+ ".sh",
31
+ ".bash",
32
+ ".zsh",
33
+ ".ps1",
34
+ ".env",
35
+ ".example"
36
+ ]);
37
+
38
+ function extnameLoose(path) {
39
+ const name = path.toLowerCase();
40
+ if (name.endsWith(".env") || name.includes(".env.")) return ".env";
41
+ const index = name.lastIndexOf(".");
42
+ return index === -1 ? "" : name.slice(index);
43
+ }
44
+
45
+ export async function discoverFiles(root) {
46
+ const files = [];
47
+
48
+ async function walk(dir) {
49
+ const entries = await readdir(dir, { withFileTypes: true });
50
+ for (const entry of entries) {
51
+ if (DEFAULT_IGNORES.has(entry.name)) continue;
52
+ const fullPath = join(dir, entry.name);
53
+ if (entry.isDirectory()) {
54
+ await walk(fullPath);
55
+ continue;
56
+ }
57
+ if (!entry.isFile()) continue;
58
+
59
+ const info = await stat(fullPath);
60
+ if (info.size > MAX_FILE_SIZE_BYTES) continue;
61
+ const ext = extnameLoose(fullPath);
62
+ if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
63
+
64
+ files.push({
65
+ path: fullPath,
66
+ relative_path: relative(root, fullPath).replaceAll("\\", "/"),
67
+ size: info.size
68
+ });
69
+ }
70
+ }
71
+
72
+ await walk(root);
73
+ return files;
74
+ }