Permissions parser from GCP official documentation written in Go

Features:

Optional features:

Code architecture and high-level documentation:

Function perm_url_func parses https://cloud.google.com/iam/docs/roles-permissions/ and collects all the roles and permissions URLs, such as https://cloud.google.com/iam/docs/roles-permissions/accessapproval.

Function role_parsing_func is optional and parses all the available roles from each roles and permissions URLs.

Function perm_collection_func collects all permissions from a given roles and permissions URL and returns these in a slice of strings.

Function all_permissions_func orchestrates the collection of permissions from roles and permissions URLs and returns a slice of strings with all permissions.

Source Code

package main

import (
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"sync"

	"golang.org/x/net/html"
)

// Google cloud base URL
var gc_base_url string = "https://cloud.google.com"

// Google cloud permissions URI
var gc_permissions_uri string = "/iam/docs/roles-permissions/"

// helper function to search for specific class in HTML
func hasClass(attrs []html.Attribute, className string) bool {
	for _, attr := range attrs {
		if attr.Key == "class" {
			// Split class attribute by spaces and look for match
			classes := strings.Fields(attr.Val)
			for _, c := range classes {
				if c == className {
					return true
				}
			}
		}
	}
	return false
}

// returns a slice of URLs (strings)
// for example: https://cloud.google.com/iam/docs/roles-permissions/accessapproval
func perm_url_func() []string {
	gc_permissions_url := gc_base_url + gc_permissions_uri

	req, err := http.NewRequest("GET", gc_permissions_url, nil)
	if err != nil {
		fmt.Println("[-] http.NewRequest error in perm_url_func")
	}

	req.Header.Set("User-Agent", "	Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("[-] http.DefaultClient.Do error in perm_url_func")
	}
	defer resp.Body.Close()

	doc, err := html.Parse(resp.Body)
	if err != nil {
		fmt.Println("[-] html.Parse error in perm_url_func")
	}

	var roles_and_permissions_urls []string
	var href_parser_func func(*html.Node)

	href_parser_func = func(n *html.Node) {
		if n.Type == html.ElementNode && n.Data == "a" {
			for _, attr := range n.Attr {
				if attr.Key == "href" && strings.HasPrefix(attr.Val, gc_permissions_uri) {
					roles_and_permissions_urls = append(roles_and_permissions_urls, gc_base_url+attr.Val)
				}
			}
		}
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			href_parser_func(c)
		}
	}
	href_parser_func(doc)

	return roles_and_permissions_urls
}

// function parsing roles from HTML
func role_parsing_func(doc *html.Node, pattern_search string) []string {
	var roles []string
	var href_parser_func func(*html.Node)

	href_parser_func = func(n *html.Node) {
		if n.Type == html.ElementNode && n.Data == "a" {
			for _, attr := range n.Attr {
				if attr.Key == "href" && strings.HasPrefix(attr.Val, pattern_search) {
					parts := strings.SplitN(attr.Val, "#", 2)
					if len(parts) == 2 {
						// check if element is already in
						roles = append(roles, parts[1])
					}
				}
			}
		}
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			href_parser_func(c)
		}
	}

	href_parser_func(doc)

	// remove duplicate roles
	uniqueMap := make(map[string]bool)
	var dedup_roles []string

	for _, item := range roles {
		if !uniqueMap[item] {
			uniqueMap[item] = true
			dedup_roles = append(dedup_roles, item)
		}
	}

	return dedup_roles
}

// returns a slice of permissions (strings)
func perm_collection_func(perm_url string) []string {
	_, err := url.Parse(perm_url)
	if err != nil {
		fmt.Println("[-] url.Parse error in perm_collection_func")
	}
	//pattern_search := parsedURL.Path + "#"
	//fmt.Printf("[debug] pattern_search: %s\n", pattern_search)

	resp, err := http.Get(perm_url)
	if err != nil {
		fmt.Println("[-] http.Get error in perm_collection_func")
	}
	defer resp.Body.Close()

	doc, err := html.Parse(resp.Body)
	if err != nil {
		fmt.Println("[-] html.Parse error in perm_collection_func")
	}

	// parse roles
	//role_parsing_func(doc, pattern_search)

	var permissions []string
	var findDataText func(*html.Node)

	findDataText = func(n *html.Node) {
		if n.Type == html.ElementNode && n.Data == "h4" && hasClass(n.Attr, "permission-name") {
			for _, attr := range n.Attr {
				if attr.Key == "data-text" {
					permissions = append(permissions, attr.Val)
				}
			}
		}
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			findDataText(c)
		}
	}

	findDataText(doc)

	return permissions
}

func worker_func(id int, jobs <-chan string, results chan<- []string, wg *sync.WaitGroup) {
	defer wg.Done()
	for pemr_url := range jobs {
		perms := perm_collection_func(pemr_url)
		results <- perms
	}
}

func all_permissions_func(perm_urls []string) []string {
	const cWorkers = 10
	jobs := make(chan string, len(perm_urls))
	results := make(chan []string, len(perm_urls))

	var wg sync.WaitGroup

	for i := 0; i < cWorkers; i++ {
		wg.Add(1)
		go worker_func(i, jobs, results, &wg)
	}

	fmt.Println("[+] Collecting permissions from GCP documentation...")
	for _, url := range perm_urls {
		jobs <- url
	}
	close(jobs)

	wg.Wait()

	close(results)

	// create slice of strings
	var permissions []string
	for result := range results {
		permissions = append(permissions, result...)
	}

	return permissions
}

func main() {
	// collect permissions URLs
	fmt.Println("[+] Collecting permissions URLs from GCP documentation...")
	perm_urls := perm_url_func()

	permissions := all_permissions_func(perm_urls)

	for _, permission := range permissions {
		fmt.Println(permission)
	}

	fmt.Printf("[*] %d\n", len(permissions))

	/*
		// write output to file
		content := strings.Join(permissions, "\n")
		// Write to a file
		err := os.WriteFile("all_permissions.txt", []byte(content), 0644)
		if err != nil {
			fmt.Println("Error writing file:", err)
			return
		}
	*/

	/*
		// read permissions from file
		file, err := os.Open("all_permissions.txt")
		if err != nil {
			fmt.Println("Error opening file:", err)
			return
		}
		defer file.Close()

		scanner := bufio.NewScanner(file)

		var lines []string
		for scanner.Scan() {
			line := scanner.Text()
			fmt.Println("Read line:", line)
			// Store lines in a slice
			lines = append(lines, scanner.Text())
		}
	*/
}


tags:#GCP - Google Cloud Platform