Skip to main content

CLI Tool for managing Microsoft Edge Enterprise Site List (SiteList.xml) written in Go

Go SiteList

Microsoft Edge can be configured to open certain websites in Internet Explorer Mode aka Compatibility Mode. This tool creates and updates the required XML file while ensuring the correct syntax.

Example:

<site-list version="2">
  <created-by>
    <tool>Manual</tool>
    <version>1</version>
    <date-created>20260812.000000</date-created>
  </created-by>
  <site url="host.domain.tld">
    <compat-mode>IE11</compat-mode>
    <open-in>IE11</open-in>
  </site>
</site-list>

Deploy Microsoft Edge Policies (via Group Policy Object) or adjust the following registry keys to enable Enterprise Site List for Internet Explorer Compatibility. Settings can be made via HKLM or HKCU; HKLM takes precedence over HKCU. Adjust the path to your SiteList.xml. It can be located on a local drive, a network share, or web server.

$path = "HKCU:\SOFTWARE\Policies\Microsoft\Edge"
New-Item -Path $path -Force
New-ItemProperty -Path $path -Name "InternetExplorerIntegrationLevel" -PropertyType DWORD -Value 1 -Force
New-ItemProperty -Path $path -Name "InternetExplorerIntegrationSiteList" -PropertyType String -Value "C:\SiteList.xml" -Force

Use the following URI to check the content and settings of the currently loaded site list file:

edge://policy/

Check with following URI the content and settings of currently loaded sitelist file:

edge://compat/enterprise

Source Code
#

package main

import (
	"encoding/xml"
	"errors"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"
)

const (
	app     = "sitelist"
	version = "1.0.0"
	author  = "Marcus Andreas Fölling <[email protected]>"
	github  = "https://github.com/marcusfoelling/sitelist"
)

type CreatedBy struct {
	Tool        string `xml:"tool"`
	Version     string `xml:"version"`
	DateCreated string `xml:"date-created"`
}

type Site struct {
	URL        string `xml:"url,attr"`
	CompatMode string `xml:"compat-mode,omitempty"`
	OpenIn     string `xml:"open-in,omitempty"`
}

type SiteList struct {
	XMLName   xml.Name  `xml:"site-list"`
	Version   string    `xml:"version,attr"`
	CreatedBy CreatedBy `xml:"created-by"`
	Sites     []Site    `xml:"site"`
}

func main() {
	fmt.Printf("%s v%s | %s | %s\n\n", app, version, author, github)

	file := flag.String("file", "./sitelist.xml", "Pfad zur XML-Datei")
	add := flag.String("add", "", "Domain zur Sitelist hinzufügen")
	delete := flag.String("delete", "", "Domain aus der Sitelist entfernen")
	flag.Parse()

	if *add != "" && *delete != "" {
		exitError(errors.New("add und delete dürfen nicht gleichzeitig verwendet werden"))
	}
	if *add == "" && *delete == "" {
		flag.Usage()
		exitError(errors.New("entweder -add oder -delete muss angegeben werden"))
	}

	sitelist, err := loadSiteList(*file)
	if err != nil {
		exitError(err)
	}

	switch {
	case *add != "":
		domain := normalizeDomain(*add)
		if err := validateDomain(domain); err != nil {
			exitError(err)
		}
		for _, site := range sitelist.Sites {
			if site.URL == domain {
				fmt.Printf("Domain bereits vorhanden: %s\n", domain)
				return
			}
		}
		site := Site{
			URL:        domain,
			CompatMode: "IE11",
			OpenIn:     "IE11",
		}
		sitelist.Sites = append(sitelist.Sites, site)
		fmt.Printf("Domain hinzugefügt: %s\n", domain)

	case *delete != "":
		domain := normalizeDomain(*delete)
		if err := validateDomain(domain); err != nil {
			exitError(err)
		}
		updatedSite := make([]Site, 0, len(sitelist.Sites))
		found := false
		for _, site := range sitelist.Sites {
			if site.URL == domain {
				found = true
				continue
			}
			updatedSite = append(updatedSite, site)
		}
		if !found {
			fmt.Printf("Domain nicht gefunden: %s\n", domain)
			return
		}
		sitelist.Sites = updatedSite
		fmt.Printf("Domain entfernt: %s\n", domain)
	}

	if err := saveSiteList(*file, sitelist); err != nil {
		exitError(err)
	}
}

func loadSiteList(filename string) (SiteList, error) {
	data, err := os.ReadFile(filename)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return SiteList{
				Version: "2",
				CreatedBy: CreatedBy{
					Tool:        filepath.Base(os.Args[0]),
					Version:     "0",
					DateCreated: time.Now().Format(time.RFC3339)},
			}, nil
		}
		return SiteList{}, fmt.Errorf("XML-Datei konnte nicht gelesen werden: %w", err)
	}

	var sitelist SiteList
	if err := xml.Unmarshal(data, &sitelist); err != nil {
		return SiteList{}, fmt.Errorf("XML-Datei ist ungültig: %w", err)
	}
	return sitelist, nil
}

func saveSiteList(filename string, sitelist SiteList) error {
	newVersion, err := incrementVersion(sitelist.CreatedBy.Version)
	if err != nil {
		newVersion = "1"
	}
	sitelist.CreatedBy.Version = newVersion
	sitelist.CreatedBy.Tool = filepath.Base(os.Args[0])
	sitelist.CreatedBy.DateCreated = time.Now().Format(time.RFC3339)
	sort.Slice(sitelist.Sites, func(i, j int) bool {
		return sitelist.Sites[i].URL < sitelist.Sites[j].URL
	})

	data, err := xml.MarshalIndent(sitelist, "", "  ")
	if err != nil {
		return fmt.Errorf("XML konnte nicht erzeugt werden: %w", err)
	}
	data = append([]byte(xml.Header), data...)

	dir := filepath.Dir(filename)
	tempFile, err := os.CreateTemp(dir, ".sitelist-*.tmp")
	if err != nil {
		return fmt.Errorf("temporäre Datei konnte nicht erstellt werden: %w", err)
	}
	tempName := tempFile.Name()

	defer func() {
		_ = tempFile.Close()
		_ = os.Remove(tempName)
	}()

	if _, err := tempFile.Write(data); err != nil {
		return fmt.Errorf("temporäre Datei konnte nicht geschrieben werden: %w", err)
	}
	if err := tempFile.Close(); err != nil {
		return fmt.Errorf("temporäre Datei konnte nicht geschlossen werden: %w", err)
	}
	if err := os.Rename(tempName, filename); err != nil {
		return fmt.Errorf("XML-Datei konnte nicht ersetzt werden: %w", err)
	}
	return nil
}

func incrementVersion(version string) (string, error) {
	v, err := strconv.Atoi(version)
	if err != nil {
		return "", fmt.Errorf("ungültige Version %q: %w", version, err)
	}
	v++
	return strconv.Itoa(v), nil
}

func normalizeDomain(domain string) string {
	domain = strings.TrimSpace(domain)
	domain = strings.ToLower(domain)
	domain = strings.TrimSuffix(domain, ".")
	return domain
}

func validateDomain(domain string) error {
	if domain == "" {
		return errors.New("Domain darf nicht leer sein")
	}
	if strings.ContainsAny(domain, " /\\@:") {
		return fmt.Errorf("ungültiger Domainname: %q", domain)
	}
	if !strings.Contains(domain, ".") {
		return fmt.Errorf("ungültiger Domainname: %q", domain)
	}
	return nil
}

func exitError(err error) {
	fmt.Fprintln(os.Stderr, "Fehler:", err)
	os.Exit(1)
}