mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-21 23:00:42 -08:00
Signed-off-by: knqyf263 <knqyf263@gmail.com> Co-authored-by: Nikita Pivkin <nikita.pivkin@smartforce.io> Co-authored-by: simar7 <1254783+simar7@users.noreply.github.com>
272 lines
6.0 KiB
Go
272 lines
6.0 KiB
Go
//go:build mage_cloudactions
|
|
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/antchfx/htmlquery"
|
|
"github.com/aquasecurity/trivy/pkg/log"
|
|
"golang.org/x/net/html"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
const (
|
|
serviceAuthURL = "https://docs.aws.amazon.com/service-authorization/latest/reference/"
|
|
serviceActionReferencesURL = "https://docs.aws.amazon.com/service-authorization/latest/reference/reference_policies_actions-resources-contextkeys.html"
|
|
targetFile = "pkg/iac/providers/aws/iam/actions.go"
|
|
defaultParallel = 10
|
|
)
|
|
|
|
func parseServiceURLs(doc *html.Node) ([]string, error) {
|
|
nodes, err := htmlquery.QueryAll(doc, `//div[@class="highlights"]/ul/li/a/@href`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to search nodes: %w\n", err)
|
|
}
|
|
|
|
res := make([]string, 0, len(nodes))
|
|
|
|
for _, node := range nodes {
|
|
// <a href="./list_awsaccountmanagement.html">AWS Account Management</a>
|
|
if node.FirstChild != nil {
|
|
res = append(res, serviceAuthURL+node.FirstChild.Data[2:])
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func parseActions(url string) ([]string, error) {
|
|
|
|
doc, err := htmlquery.LoadURL(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
servicePrefix, err := parseServicePrefix(doc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
actions, err := parseServiceActions(doc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := make([]string, 0, len(actions))
|
|
|
|
for _, act := range actions {
|
|
res = append(res, servicePrefix+":"+act)
|
|
}
|
|
|
|
fmt.Printf("Parsing of %q actions is completed\n", servicePrefix)
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func parseServiceActions(doc *html.Node) ([]string, error) {
|
|
table, err := htmlquery.Query(doc, `//div[@class="table-container"]/div/table/tbody`)
|
|
if table == nil {
|
|
return nil, errors.New("actions table not found")
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query tables: %w\n", err)
|
|
}
|
|
|
|
var actions []string
|
|
|
|
var f func(*html.Node)
|
|
f = func(n *html.Node) {
|
|
for _, tr := range findSubtags(n, "tr") {
|
|
var action string
|
|
for k, td := range findSubtags(tr, "td") {
|
|
// first column - action
|
|
if k == 0 {
|
|
if a := findSubtag(td, "a"); a != nil && a.FirstChild != nil {
|
|
action = a.FirstChild.Data
|
|
}
|
|
|
|
// fourth column - resource type
|
|
// If the column is empty, then the action does not support resource-level permissions
|
|
// and you must specify all resources ("*") in your policy
|
|
} else if action != "" && k == 3 && td.FirstChild == nil {
|
|
actions = append(actions, action)
|
|
}
|
|
}
|
|
}
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
f(c)
|
|
}
|
|
}
|
|
f(table)
|
|
|
|
return actions, err
|
|
}
|
|
|
|
func findSubtag(n *html.Node, tagName string) *html.Node {
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
if c.Type == html.ElementNode && c.Data == tagName {
|
|
return c
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func findSubtags(n *html.Node, tagName string) []*html.Node {
|
|
var result []*html.Node
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
if c.Type == html.ElementNode && c.Data == tagName {
|
|
result = append(result, c)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func parseServicePrefix(doc *html.Node) (string, error) {
|
|
nodes, err := htmlquery.QueryAll(doc, `//div[@id="main-col-body"]/p/descendant-or-self::*/text()`)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to query paragraph: %w\n", err)
|
|
}
|
|
|
|
var sb strings.Builder
|
|
for _, node := range nodes {
|
|
sb.WriteString(node.Data)
|
|
}
|
|
|
|
p := sb.String()
|
|
sb.Reset()
|
|
|
|
idx := strings.Index(p, "service prefix: ")
|
|
if idx == -1 {
|
|
return "", fmt.Errorf("failed extract service prefix from text: %s\n", p)
|
|
}
|
|
idx += len("service prefix: ")
|
|
|
|
if len(p)-1 <= idx {
|
|
return "", fmt.Errorf("failed to parse service prefix from text: %s\n", p)
|
|
}
|
|
|
|
var parsed bool
|
|
for _, r := range p[idx:] {
|
|
if r == ')' {
|
|
parsed = true
|
|
break
|
|
}
|
|
sb.WriteRune(r)
|
|
}
|
|
|
|
if !parsed {
|
|
return "", fmt.Errorf("failed to parse service prefix from text: %s\n", p)
|
|
}
|
|
|
|
return sb.String(), nil
|
|
}
|
|
|
|
func generateFile(path string, actions []string) error {
|
|
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create file: %w\n", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
w := bufio.NewWriter(f)
|
|
_, _ = w.WriteString(
|
|
`// Code generated by mage genallowedactions DO NOT EDIT.
|
|
|
|
package iam
|
|
|
|
var allowedActionsForResourceWildcardsMap = map[string]struct{}{
|
|
`,
|
|
)
|
|
|
|
for _, action := range actions {
|
|
_, _ = w.WriteString("\t\"" + action + "\": {},\n")
|
|
}
|
|
_, _ = w.WriteString("}")
|
|
|
|
return w.Flush()
|
|
}
|
|
|
|
func main() {
|
|
if err := GenAllowedActions(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// GenAllowedActions generates the list of valid actions for wildcard support
|
|
func GenAllowedActions() error {
|
|
log.Info("Start parsing actions")
|
|
startTime := time.Now()
|
|
defer func() {
|
|
log.Info("Parsing is completed", log.Duration(time.Since(startTime).Seconds()))
|
|
}()
|
|
|
|
doc, err := htmlquery.LoadURL(serviceActionReferencesURL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to retrieve action references: %w\n", err)
|
|
}
|
|
urls, err := parseServiceURLs(doc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
g, ctx := errgroup.WithContext(context.TODO())
|
|
g.SetLimit(defaultParallel)
|
|
|
|
// actions may be the same for services of different versions,
|
|
// e.g. Elastic Load Balancing and Elastic Load Balancing V2
|
|
actionsSet := make(map[string]struct{})
|
|
|
|
var mu sync.Mutex
|
|
|
|
for _, url := range urls {
|
|
url := url
|
|
if ctx.Err() != nil {
|
|
break
|
|
}
|
|
g.Go(func() error {
|
|
serviceActions, err := parseActions(url)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse actions from %q: %w\n", url, err)
|
|
}
|
|
|
|
mu.Lock()
|
|
for _, act := range serviceActions {
|
|
actionsSet[act] = struct{}{}
|
|
}
|
|
mu.Unlock()
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
if err := g.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
actions := make([]string, 0, len(actionsSet))
|
|
|
|
for act := range actionsSet {
|
|
actions = append(actions, act)
|
|
}
|
|
|
|
sort.Strings(actions)
|
|
|
|
path := filepath.FromSlash(targetFile)
|
|
if err := generateFile(path, actions); err != nil {
|
|
return fmt.Errorf("failed to generate file: %w\n", err)
|
|
}
|
|
return nil
|
|
}
|