47fac22230
- .claude/CLAUDE.md - .claude/commands/subagentes.md - .claude/rules/INDEX.md - .mcp.json - bash/functions/cybersecurity/analyze_dns.md - bash/functions/cybersecurity/audit_http_headers.md - bash/functions/cybersecurity/audit_ssh_config.md - bash/functions/cybersecurity/check_firewall.md - bash/functions/cybersecurity/detect_suspicious_users.md - bash/functions/cybersecurity/encrypt_file.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
210 lines
6.0 KiB
Go
210 lines
6.0 KiB
Go
package infra
|
|
|
|
import (
|
|
"bufio"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// CapabilityGroupAudit holds the audit result for a single capability group.
|
|
type CapabilityGroupAudit struct {
|
|
Group string `json:"group"` // slug del grupo (tag canonico)
|
|
DeclaredInIndex bool `json:"declared_in_index"` // aparece en docs/capabilities/INDEX.md
|
|
DocExists bool `json:"doc_exists"` // existe docs/capabilities/<group>.md
|
|
FunctionCount int `json:"function_count"` // funciones con ese tag en registry.db
|
|
Issues []string `json:"issues"` // lista de problemas detectados
|
|
OK bool `json:"ok"` // true si Issues esta vacio
|
|
}
|
|
|
|
// indexLinkRe matches markdown links [slug](target.md). Go regexp has no
|
|
// backreferences, so we capture both sides and compare in code.
|
|
var indexLinkRe = regexp.MustCompile(`\[([a-z][a-z0-9_-]*)\]\(([a-z][a-z0-9_-]*)\.md\)`)
|
|
|
|
// AuditCapabilityGroups audits the drift between:
|
|
// 1. Groups declared in docs/capabilities/INDEX.md (parsed via link regex).
|
|
// 2. Tags present in functions.tags of registry.db (via json_each).
|
|
// 3. Doc pages existing as docs/capabilities/<group>.md on disk.
|
|
//
|
|
// Returns one CapabilityGroupAudit per group/candidate, ordered by Group ASC.
|
|
// Tags with count >= 3 that are not declared in INDEX are surfaced as
|
|
// "ungrouped_candidate". Doc files not listed in INDEX are surfaced as
|
|
// "doc_orphan".
|
|
func AuditCapabilityGroups(root string) ([]CapabilityGroupAudit, error) {
|
|
// --- 1. Open registry.db (read-only) ---
|
|
dbPath := filepath.Join(root, "registry.db")
|
|
dsn := fmt.Sprintf("file:%s?mode=ro&_foreign_keys=on", dbPath)
|
|
db, err := sql.Open("sqlite3", dsn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("audit_capability_groups: open db: %w", err)
|
|
}
|
|
defer db.Close()
|
|
if err := db.Ping(); err != nil {
|
|
return nil, fmt.Errorf("audit_capability_groups: ping db: %w", err)
|
|
}
|
|
|
|
// --- 2. Query all tags with their function counts ---
|
|
tagCounts, err := queryTagCounts(db)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("audit_capability_groups: query tags: %w", err)
|
|
}
|
|
|
|
// --- 3. Parse INDEX.md for declared group slugs ---
|
|
indexPath := filepath.Join(root, "docs", "capabilities", "INDEX.md")
|
|
indexSlugs, err := parseIndexSlugs(indexPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("audit_capability_groups: parse INDEX.md: %w", err)
|
|
}
|
|
|
|
// --- 4. Scan docs/capabilities/ for existing .md files (excluding INDEX.md) ---
|
|
capDir := filepath.Join(root, "docs", "capabilities")
|
|
docSlugs, err := scanDocSlugs(capDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("audit_capability_groups: scan docs: %w", err)
|
|
}
|
|
|
|
// --- 5. Build audit entries ---
|
|
// We collect all relevant slugs: declared in INDEX, present as doc, or tags >= 3.
|
|
allSlugs := make(map[string]struct{})
|
|
for s := range indexSlugs {
|
|
allSlugs[s] = struct{}{}
|
|
}
|
|
for s := range docSlugs {
|
|
allSlugs[s] = struct{}{}
|
|
}
|
|
for tag, count := range tagCounts {
|
|
if count >= 3 {
|
|
allSlugs[tag] = struct{}{}
|
|
}
|
|
}
|
|
|
|
results := make([]CapabilityGroupAudit, 0, len(allSlugs))
|
|
for slug := range allSlugs {
|
|
_, declaredInIndex := indexSlugs[slug]
|
|
_, docExists := docSlugs[slug]
|
|
count := tagCounts[slug]
|
|
|
|
var issues []string
|
|
if declaredInIndex {
|
|
if !docExists {
|
|
issues = append(issues, "doc_missing")
|
|
}
|
|
if count == 0 {
|
|
issues = append(issues, "no_functions")
|
|
} else if count < 3 {
|
|
issues = append(issues, "below_minimum")
|
|
}
|
|
} else if docExists {
|
|
// Doc exists but not in INDEX
|
|
issues = append(issues, "doc_orphan")
|
|
} else if count >= 3 {
|
|
// Tag with enough functions but no declaration or doc
|
|
issues = append(issues, "ungrouped_candidate")
|
|
}
|
|
|
|
results = append(results, CapabilityGroupAudit{
|
|
Group: slug,
|
|
DeclaredInIndex: declaredInIndex,
|
|
DocExists: docExists,
|
|
FunctionCount: count,
|
|
Issues: issues,
|
|
OK: len(issues) == 0,
|
|
})
|
|
}
|
|
|
|
sort.Slice(results, func(i, j int) bool {
|
|
return results[i].Group < results[j].Group
|
|
})
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// queryTagCounts returns a map of tag → function count using json_each on functions.tags.
|
|
func queryTagCounts(db *sql.DB) (map[string]int, error) {
|
|
rows, err := db.Query(`
|
|
SELECT j.value AS tag, COUNT(*) AS cnt
|
|
FROM functions f, json_each(f.tags) j
|
|
GROUP BY j.value
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
counts := make(map[string]int)
|
|
for rows.Next() {
|
|
var tag string
|
|
var cnt int
|
|
if err := rows.Scan(&tag, &cnt); err != nil {
|
|
continue
|
|
}
|
|
counts[tag] = cnt
|
|
}
|
|
return counts, rows.Err()
|
|
}
|
|
|
|
// parseIndexSlugs reads INDEX.md and extracts group slugs from links of the
|
|
// form [slug](slug.md). Returns the set of declared slugs.
|
|
func parseIndexSlugs(indexPath string) (map[string]struct{}, error) {
|
|
f, err := os.Open(indexPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// No INDEX.md — return empty set, not an error.
|
|
return make(map[string]struct{}), nil
|
|
}
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
slugs := make(map[string]struct{})
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
matches := indexLinkRe.FindAllStringSubmatch(line, -1)
|
|
for _, m := range matches {
|
|
// Self-referencing link only: [slug](slug.md), not external.
|
|
label := strings.TrimSpace(m[1])
|
|
target := strings.TrimSpace(m[2])
|
|
if label != "" && label == target {
|
|
slugs[label] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
return slugs, scanner.Err()
|
|
}
|
|
|
|
// scanDocSlugs returns the set of slugs that have a corresponding .md file in
|
|
// capDir, excluding INDEX.md itself.
|
|
func scanDocSlugs(capDir string) (map[string]struct{}, error) {
|
|
entries, err := os.ReadDir(capDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return make(map[string]struct{}), nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
slugs := make(map[string]struct{})
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
continue
|
|
}
|
|
name := e.Name()
|
|
if !strings.HasSuffix(name, ".md") {
|
|
continue
|
|
}
|
|
slug := strings.TrimSuffix(name, ".md")
|
|
if slug == "INDEX" {
|
|
continue
|
|
}
|
|
slugs[slug] = struct{}{}
|
|
}
|
|
return slugs, nil
|
|
}
|