Repository: ysugimoto/falco
Pull request #102 (2022-09-11 21:14:02)

Adds support for parsing Dictionary/ACL/Backend from TF json
Some properties can be defined externally in TF and then be used from the VCL file. We add support for parsing them from the TF json file and adding them to the json context.

Signed-off-by: Sotiris Nanopoulos <sotiris.nanopoulos@reddit.com>

davinci26: >What would happen if we have double declarations (another declaration in the VCL) Should we address this case?

This was handled already but I also added a test
davinci26: See https://reddit.atlassian.net/wiki/spaces/TPORT/pages/2421096466/Developing+deploying+a+Fastly+service and if you have any question reach out to #infra-transport or open a ticket at [go/transport-ticket](https://go.snooguts.net/transport-ticket)
ivomurrell: In retrospect, I should probably have renamed `snippets.go` to something more descriptive in #95 as its utility has grown beyond snippets now...
davinci26: @ivomurrell WDYT of the current approach?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c09c63..1fca4d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## v0.16.0
+
+- Allow req.backend to be read as a string [#96](https://github.com/ysugimoto/falco/pull/96) (@ivomurrell)
+- Combine included VCL modules into main and parse once [#93](https://github.com/ysugimoto/falco/pull/93) (@ysugimoto)
+- Add missing backend property [#100](https://github.com/ysugimoto/falco/pull/100) (@davinci26)
+- Adds Tests for examples [#101](https://github.com/ysugimoto/falco/pull/101) (@davinci26)
+
 ## v0.15.0
 
 - Allow function expressions in error statements [#94](https://github.com/ysugimoto/falco/pull/94) (@ivomurrell)
diff --git a/cmd/falco/main.go b/cmd/falco/main.go
index 335d6cf..5055f00 100644
--- a/cmd/falco/main.go
+++ b/cmd/falco/main.go
@@ -12,6 +12,7 @@ import (
 	"github.com/kyokomi/emoji"
 	"github.com/mattn/go-colorable"
 	"github.com/pkg/errors"
+	"github.com/ysugimoto/falco/terraform"
 )
 
 var version string = ""
@@ -136,11 +137,16 @@ func main() {
 		os.Exit(1)
 	}
 
+	var fetcher Fetcher
 	// falco could lint multiple services so resolver should be a slice
 	var resolvers []Resolver
 	switch fs.Arg(0) {
 	case subcommandTerraform:
-		resolvers, err = NewStdinResolvers()
+		fastlyServices, err := ParseStdin()
+		if err == nil {
+			resolvers = NewTerraformResolver(fastlyServices)
+			fetcher = terraform.NewTerraformFetcher(fastlyServices)
+		}
 	case subcommandLint:
 		// "lint" command provides single file of service, then resolvers size is always 1
 		resolvers, err = NewFileResolvers(fs.Arg(1), c)
@@ -161,7 +167,7 @@ func main() {
 			writeln(white, strings.Repeat("=", 18+len(name)))
 		}
 
-		runner, err := NewRunner(v, c)
+		runner, err := NewRunner(v, c, fetcher)
 		if err != nil {
 			writeln(red, err.Error())
 			os.Exit(1)
diff --git a/cmd/falco/resolver.go b/cmd/falco/resolver.go
index 473437f..dfee329 100644
--- a/cmd/falco/resolver.go
+++ b/cmd/falco/resolver.go
@@ -1,15 +1,6 @@
 package main
 
 import (
-	"bytes"
-	"fmt"
-	"os"
-	"strings"
-	"time"
-
-	"io/ioutil"
-	"path/filepath"
-
 	"github.com/pkg/errors"
 )
 
@@ -22,167 +13,10 @@ type VCL struct {
 	Data string
 }
 
-// Resolver is an interface for integrate VCL input from file or JSON (terraform planned data)
+// Resolver is an interface to fetch VCL source and dependencies
+// from various sources e.g. file or JSON (terraform planned data)
 type Resolver interface {
 	MainVCL() (*VCL, error)
 	Resolve(module string) (*VCL, error)
 	Name() string
 }
-
-// FileResolver is filesystem resolver, basically used for built vcl files
-type FileResolver struct {
-	main         string
-	includePaths []string
-}
-
-func NewFileResolvers(main string, c *Config) ([]Resolver, error) {
-	if main == "" {
-		return nil, ErrEmptyMain
-	}
-
-	if _, err := os.Stat(main); err != nil {
-		if err == os.ErrNotExist {
-			return nil, errors.New(fmt.Sprintf("Input file %s is not found", main))
-		}
-		return nil, errors.New(fmt.Sprintf("Unexpected stat error: %s", err.Error()))
-	}
-
-	abs, err := filepath.Abs(main)
-	if err != nil {
-		return nil, errors.New(fmt.Sprintf("Failed to get absolute path: %s", err.Error()))
-	}
-
-	var includePaths []string
-	// Add include paths as absolute
-	for i := range c.IncludePaths {
-		p, err := filepath.Abs(c.IncludePaths[i])
-		if err == nil {
-			includePaths = append(includePaths, p)
-		}
-	}
-	includePaths = append(includePaths, filepath.Dir(abs))
-
-	return []Resolver{
-		&FileResolver{
-			main:         abs,
-			includePaths: includePaths,
-		},
-	}, nil
-}
-
-func (f *FileResolver) Name() string {
-	return ""
-}
-
-func (f *FileResolver) getVCL(file string) (*VCL, error) {
-	if _, err := os.Stat(file); err != nil {
-		return nil, err
-	}
-	fp, err := os.Open(file)
-	if err != nil {
-		return nil, err
-	}
-	defer fp.Close()
-
-	buf := &bytes.Buffer{}
-	if _, err := buf.ReadFrom(fp); err != nil {
-		return nil, err
-	}
-
-	return &VCL{
-		Name: file,
-		Data: buf.String(),
-	}, nil
-}
-
-func (f *FileResolver) MainVCL() (*VCL, error) {
-	return f.getVCL(f.main)
-}
-
-func (f *FileResolver) Resolve(module string) (*VCL, error) {
-	module_path_with_extension := module
-	if !strings.HasSuffix(module_path_with_extension, ".vcl") {
-		module_path_with_extension += ".vcl"
-	}
-
-	// Find for each include paths
-	for _, p := range f.includePaths {
-		if vcl, err := f.getVCL(filepath.Join(p, module_path_with_extension)); err == nil {
-			return vcl, nil
-		}
-	}
-	return nil, errors.New(fmt.Sprintf("Failed to resolve include file: %s.vcl", module))
-}
-
-// StdinResolver is in memory resolver, read and factory vcl data from terraform planned JSON input
-type StdinResolver struct {
-	Modules     []*VCL
-	Main        *VCL
-	ServiceName string
-}
-
-func NewStdinResolvers() ([]Resolver, error) {
-	// Consider reading from stdin timeout to not to hang up in CI flow
-	input := make(chan []byte)
-	errChan := make(chan error)
-
-	go func() {
-		buf, err := ioutil.ReadAll(os.Stdin)
-		if err != nil {
-			errChan <- err
-			return
-		}
-		input <- buf
-	}()
-
-	var resolvers []Resolver
-	select {
-	case buf := <-input:
-		services, err := unmarshalTerraformPlannedInput(buf)
-		if err != nil {
-			return nil, err
-		}
-		for _, v := range services {
-			s := &StdinResolver{
-				ServiceName: v.Name,
-			}
-			for _, vcl := range v.Vcls {
-				if vcl.Main {
-					s.Main = &VCL{
-						Name: vcl.Name,
-						Data: vcl.Content,
-					}
-				} else {
-					s.Modules = append(s.Modules, &VCL{
-						Name: vcl.Name,
-						Data: vcl.Content,
-					})
-				}
-			}
-			resolvers = append(resolvers, s)
-		}
-		return resolvers, nil
-	case err := <-errChan:
-		return nil, errors.New(fmt.Sprintf("Failed to read from stdin: %s", err.Error()))
-	case <-time.After(10 * time.Second):
-		return nil, errors.New(("Failed to read from stdin: timed out"))
-	}
-}
-
-func (s *StdinResolver) Name() string {
-	return s.ServiceName
-}
-
-func (s *StdinResolver) MainVCL() (*VCL, error) {
-	return s.Main, nil
-}
-
-func (s *StdinResolver) Resolve(module string) (*VCL, error) {
-	for i := range s.Modules {
-		if s.Modules[i].Name == module {
-			return s.Modules[i], nil
-		}
-	}
-
-	return nil, errors.New(fmt.Sprintf("Failed to resolve include file: %s.vcl", module))
-}
diff --git a/cmd/falco/resolver_file.go b/cmd/falco/resolver_file.go
new file mode 100644
index 0000000..f6c6bc6
--- /dev/null
+++ b/cmd/falco/resolver_file.go
@@ -0,0 +1,99 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"strings"
+
+	"path/filepath"
+
+	"github.com/pkg/errors"
+)
+
+// FileResolver is filesystem resolver, basically used for built vcl files
+type FileResolver struct {
+	main         string
+	includePaths []string
+}
+
+func NewFileResolvers(main string, c *Config) ([]Resolver, error) {
+	if main == "" {
+		return nil, ErrEmptyMain
+	}
+
+	if _, err := os.Stat(main); err != nil {
+		if err == os.ErrNotExist {
+			return nil, errors.New(fmt.Sprintf("Input file %s is not found", main))
+		}
+		return nil, errors.New(fmt.Sprintf("Unexpected stat error: %s", err.Error()))
+	}
+
+	abs, err := filepath.Abs(main)
+	if err != nil {
+		return nil, errors.New(fmt.Sprintf("Failed to get absolute path: %s", err.Error()))
+	}
+
+	var includePaths []string
+	// Add include paths as absolute
+	for i := range c.IncludePaths {
+		p, err := filepath.Abs(c.IncludePaths[i])
+		if err == nil {
+			includePaths = append(includePaths, p)
+		}
+	}
+	includePaths = append(includePaths, filepath.Dir(abs))
+
+	return []Resolver{
+		&FileResolver{
+			main:         abs,
+			includePaths: includePaths,
+		},
+	}, nil
+}
+
+func (f *FileResolver) Name() string {
+	return ""
+}
+
+func (f *FileResolver) getVCL(file string) (*VCL, error) {
+	if _, err := os.Stat(file); err != nil {
+		return nil, err
+	}
+
+	fp, err := os.Open(file)
+	if err != nil {
+		return nil, err
+	}
+	defer fp.Close()
+
+	buf := &bytes.Buffer{}
+	if _, err := buf.ReadFrom(fp); err != nil {
+		return nil, err
+	}
+
+	return &VCL{
+		Name: file,
+		Data: buf.String(),
+	}, nil
+}
+
+func (f *FileResolver) MainVCL() (*VCL, error) {
+	return f.getVCL(f.main)
+}
+
+func (f *FileResolver) Resolve(module string) (*VCL, error) {
+	modulePathWithExtension := module
+	if !strings.HasSuffix(modulePathWithExtension, ".vcl") {
+		modulePathWithExtension += ".vcl"
+	}
+
+	// Find for each include paths
+	for _, p := range f.includePaths {
+		if vcl, err := f.getVCL(filepath.Join(p, modulePathWithExtension)); err == nil {
+			return vcl, nil
+		}
+	}
+
+	return nil, errors.New(fmt.Sprintf("Failed to resolve include file: %s.vcl", module))
+}
diff --git a/cmd/falco/resolver_terraform.go b/cmd/falco/resolver_terraform.go
new file mode 100644
index 0000000..2d159ca
--- /dev/null
+++ b/cmd/falco/resolver_terraform.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+	"fmt"
+
+	"github.com/pkg/errors"
+	"github.com/ysugimoto/falco/terraform"
+)
+
+// TerraformResolver is in memory resolver, read and factory vcl data from terraform planned JSON input
+type TerraformResolver struct {
+	Modules     []*VCL
+	Main        *VCL
+	ServiceName string
+}
+
+func NewTerraformResolver(services []*terraform.FastlyService) []Resolver {
+	var resolvers []Resolver
+	for _, v := range services {
+		s := &TerraformResolver{
+			ServiceName: v.Name,
+		}
+		for _, vcl := range v.Vcls {
+			if vcl.Main {
+				s.Main = &VCL{
+					Name: vcl.Name,
+					Data: vcl.Content,
+				}
+			} else {
+				s.Modules = append(s.Modules, &VCL{
+					Name: vcl.Name,
+					Data: vcl.Content,
+				})
+			}
+		}
+		resolvers = append(resolvers, s)
+	}
+	return resolvers
+}
+
+func (s *TerraformResolver) Name() string {
+	return s.ServiceName
+}
+
+func (s *TerraformResolver) MainVCL() (*VCL, error) {
+	return s.Main, nil
+}
+
+func (s *TerraformResolver) Resolve(module string) (*VCL, error) {
+	for i := range s.Modules {
+		if s.Modules[i].Name == module {
+			return s.Modules[i], nil
+		}
+	}
+
+	return nil, errors.New(fmt.Sprintf("Failed to resolve include file: %s.vcl", module))
+}
diff --git a/cmd/falco/runner.go b/cmd/falco/runner.go
index d23a3de..fb87bf5 100644
--- a/cmd/falco/runner.go
+++ b/cmd/falco/runner.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"bytes"
-	_context "context"
 	"fmt"
 	"os"
 	"strings"
@@ -18,6 +17,8 @@ import (
 	"github.com/ysugimoto/falco/linter"
 	"github.com/ysugimoto/falco/parser"
 	"github.com/ysugimoto/falco/plugin"
+	"github.com/ysugimoto/falco/remote"
+	"github.com/ysugimoto/falco/types"
 )
 
 var (
@@ -51,6 +52,12 @@ type StatsResult struct {
 	Lines       int    `json:"lines"`
 }
 
+type Fetcher interface {
+	Backends() ([]*types.RemoteBackend, error)
+	Dictionaries() ([]*types.RemoteDictionary, error)
+	Acls() ([]*types.RemoteAcl, error)
+}
+
 type RunMode int
 
 const (
@@ -76,7 +83,7 @@ type Runner struct {
 	errors   int
 }
 
-func NewRunner(rz Resolver, c *Config) (*Runner, error) {
+func NewRunner(rz Resolver, c *Config, f Fetcher) (*Runner, error) {
 	r := &Runner{
 		resolver:  rz,
 		context:   context.New(),
@@ -99,11 +106,10 @@ func NewRunner(rz Resolver, c *Config) (*Runner, error) {
 			return nil, errors.New("Both FASTLY_SERVICE_ID and FASTLY_API_KEY environment variables must be specified")
 		}
 		func() {
-			ctx, timeout := _context.WithTimeout(_context.Background(), 20*time.Second)
-			defer timeout()
-
 			// Remote communication is optional so we keep processing even if remote communication is failed
-			snippets, err := NewSnippet(serviceId, apiKey).Fetch(ctx)
+			// We allow each API call to take up to to 5 seconds
+			f := remote.NewFastlyApiFetcher(serviceId, apiKey, 5*time.Second)
+			snippets, err := NewSnippet(f).Fetch()
 			if err != nil {
 				writeln(red, err.Error())
 			}
@@ -112,6 +118,14 @@ func NewRunner(rz Resolver, c *Config) (*Runner, error) {
 		}()
 	}
 
+	if f != nil {
+		snippets, err := NewSnippet(f).Fetch()
+		if err != nil {
+			writeln(red, err.Error())
+		}
+		r.snippets = append(r.snippets, snippets...)
+	}
+
 	// Check transformer exists and format to absolute path
 	// Transformer is provided as independent binary, named "falco-transform-[name]"
 	// so, if transformer specified with "lambdaedge", program lookup "falco-transform-lambdaedge" binary existence
@@ -222,6 +236,7 @@ func (r *Runner) Run() (*RunnerResult, error) {
 	if err != nil {
 		return nil, err
 	}
+
 	vcl, err := r.run(main, RunModeLint)
 	if err != nil {
 		return nil, err
@@ -445,6 +460,7 @@ func (r *Runner) Stats() (*StatsResult, error) {
 	if err != nil {
 		return nil, err
 	}
+
 	if _, err := r.run(main, RunModeStat); err != nil {
 		return nil, err
 	}
diff --git a/cmd/falco/snippet.go b/cmd/falco/snippet.go
index 9f625e0..31e6daf 100644
--- a/cmd/falco/snippet.go
+++ b/cmd/falco/snippet.go
@@ -2,14 +2,13 @@ package main
 
 import (
 	"bytes"
-	_context "context"
 	"fmt"
 	"strings"
 
-	"net/http"
 	"text/template"
 
 	"github.com/ysugimoto/falco/remote"
+	"github.com/ysugimoto/falco/types"
 )
 
 var tableTemplate = `
@@ -46,25 +45,20 @@ type snippetItem struct {
 }
 
 type Snippet struct {
-	client *remote.FastlyClient
+	fetcher Fetcher
 }
 
-func NewSnippet(serviceId, apiKey string) *Snippet {
+func NewSnippet(f Fetcher) *Snippet {
 	return &Snippet{
-		client: remote.NewFastlyClient(http.DefaultClient, serviceId, apiKey),
+		fetcher: f,
 	}
 }
 
-func (s *Snippet) Fetch(c _context.Context) ([]snippetItem, error) {
+func (s *Snippet) Fetch() ([]snippetItem, error) {
 	var snippets []snippetItem
 
-	// Fetch latest version
-	version, err := s.client.LatestVersion(c)
-	if err != nil {
-		return nil, fmt.Errorf("Failed to get latest version %w", err)
-	}
 	write(white, "Fetching Edge Dictionaries...")
-	dicts, err := s.fetchEdgeDictionary(c, version)
+	dicts, err := s.fetchEdgeDictionary()
 	if err != nil {
 		return nil, err
 	}
@@ -72,7 +66,7 @@ func (s *Snippet) Fetch(c _context.Context) ([]snippetItem, error) {
 	snippets = append(snippets, dicts...)
 
 	write(white, "Fatching Access Control Lists...")
-	acls, err := s.fetchAccessControl(c, version)
+	acls, err := s.fetchAccessControl()
 	if err != nil {
 		return nil, err
 	}
@@ -80,7 +74,7 @@ func (s *Snippet) Fetch(c _context.Context) ([]snippetItem, error) {
 	snippets = append(snippets, acls...)
 
 	write(white, "Fatching Backends...")
-	backends, err := s.fetchBackend(c, version)
+	backends, err := s.fetchBackend()
 	if err != nil {
 		return nil, err
 	}
@@ -91,10 +85,10 @@ func (s *Snippet) Fetch(c _context.Context) ([]snippetItem, error) {
 }
 
 // Fetch remote Edge dictionary items
-func (s *Snippet) fetchEdgeDictionary(c _context.Context, version int64) ([]snippetItem, error) {
-	dicts, err := s.client.ListEdgeDictionaries(c, version)
+func (s *Snippet) fetchEdgeDictionary() ([]snippetItem, error) {
+	dicts, err := s.fetcher.Dictionaries()
 	if err != nil {
-		return nil, fmt.Errorf("Failed to get edge dictionaries for version %w", err)
+		return nil, fmt.Errorf("Failed to get edge dictionaries %w", err)
 	}
 
 	tmpl, err := template.New("table").Parse(tableTemplate)
@@ -117,22 +111,22 @@ func (s *Snippet) fetchEdgeDictionary(c _context.Context, version int64) ([]snip
 }
 
 // Fetch remote Access Control entries
-func (s *Snippet) fetchAccessControl(c _context.Context, version int64) ([]snippetItem, error) {
-	acls, err := s.client.ListAccessControlLists(c, version)
+func (s *Snippet) fetchAccessControl() ([]snippetItem, error) {
+	acls, err := s.fetcher.Acls()
 	if err != nil {
-		return nil, fmt.Errorf("Failed to get access control lists for version %w", err)
+		return nil, fmt.Errorf("Failed to get ACLs: %w", err)
 	}
 
 	tmpl, err := template.New("acl").Parse(aclTemplate)
 	if err != nil {
-		return nil, fmt.Errorf("Failed to compilte acl template: %w", err)
+		return nil, fmt.Errorf("Failed to compile acl template: %w", err)
 	}
 
 	var snippets []snippetItem
 	for _, a := range acls {
 		buf := new(bytes.Buffer)
 		if err := tmpl.Execute(buf, a); err != nil {
-			return nil, fmt.Errorf("Failed to render table template: %w", err)
+			return nil, fmt.Errorf("Failed to render acl template: %w", err)
 		}
 		snippets = append(snippets, snippetItem{
 			Name: fmt.Sprintf("ACL:%s", a.Name),
@@ -142,18 +136,20 @@ func (s *Snippet) fetchAccessControl(c _context.Context, version int64) ([]snipp
 	return snippets, nil
 }
 
-func (s *Snippet) fetchBackend(c _context.Context, version int64) ([]snippetItem, error) {
-	backends, err := s.client.ListBackends(c, version)
+func (s *Snippet) fetchBackend() ([]snippetItem, error) {
+	var snippets []snippetItem
+	backends, err := s.fetcher.Backends()
 	if err != nil {
-		return nil, fmt.Errorf("failed to get backends for version %w", err)
+		return nil, fmt.Errorf("Failed to get Backends: %w", err)
+	}
+	if len(backends) == 0 {
+		return snippets, nil
 	}
-
 	backTmpl, err := template.New("backend").Parse(backendTemplate)
 	if err != nil {
 		return nil, fmt.Errorf("failed to compile backend template: %w", err)
 	}
 
-	var snippets []snippetItem
 	for _, b := range backends {
 		buf := new(bytes.Buffer)
 		if err := backTmpl.Execute(buf, b); err != nil {
@@ -174,7 +170,7 @@ func (s *Snippet) fetchBackend(c _context.Context, version int64) ([]snippetItem
 	return snippets, nil
 }
 
-func (s *Snippet) renderBackendShields(backends []*remote.Backend) ([]snippetItem, error) {
+func (s *Snippet) renderBackendShields(backends []*types.RemoteBackend) ([]snippetItem, error) {
 	printType := func(dtype remote.DirectorType) string {
 		switch dtype {
 		case remote.Random:
@@ -195,7 +191,7 @@ func (s *Snippet) renderBackendShields(backends []*remote.Backend) ([]snippetIte
 
 	shieldDirectors := make(map[string]struct{})
 	for _, b := range backends {
-		if b.Shield != nil {
+		if b.Shield != nil && *b.Shield != "" {
 			shieldDirectors[*b.Shield] = struct{}{}
 		}
 	}
diff --git a/cmd/falco/stdin_parser.go b/cmd/falco/stdin_parser.go
new file mode 100644
index 0000000..39087e4
--- /dev/null
+++ b/cmd/falco/stdin_parser.go
@@ -0,0 +1,35 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"time"
+
+	"github.com/pkg/errors"
+	"github.com/ysugimoto/falco/terraform"
+)
+
+func ParseStdin() ([]*terraform.FastlyService, error) {
+	// Consider reading from stdin timeout to not to hang up in CI flow
+	input := make(chan []byte)
+	errChan := make(chan error)
+
+	go func() {
+		buf, err := io.ReadAll(os.Stdin)
+		if err != nil {
+			errChan <- err
+			return
+		}
+		input <- buf
+	}()
+
+	select {
+	case buf := <-input:
+		return terraform.UnmarshalTerraformPlannedInput(buf)
+	case err := <-errChan:
+		return nil, errors.New(fmt.Sprintf("Failed to read from stdin: %s", err.Error()))
+	case <-time.After(10 * time.Second):
+		return nil, errors.New(("Failed to read from stdin: timed out"))
+	}
+}
diff --git a/linter/errors.go b/linter/errors.go
index 444dc3d..f97c795 100644
--- a/linter/errors.go
+++ b/linter/errors.go
@@ -290,6 +290,16 @@ func UnusedDeclaration(m *ast.Meta, name, declType string) *LintError {
 	}
 }
 
+func UnusedExternalDeclaration(name, declType string) *LintError {
+	return &LintError{
+		Severity: WARNING,
+		Message: fmt.Sprintf(
+			`Externally defined %s "%s" is unused in VCL program`,
+			declType, name,
+		),
+	}
+}
+
 func UnusedVariable(m *ast.Meta, name string) *LintError {
 	return &LintError{
 		Severity: WARNING,
diff --git a/linter/linter.go b/linter/linter.go
index e793618..b43a973 100644
--- a/linter/linter.go
+++ b/linter/linter.go
@@ -54,32 +54,44 @@ func (l *Linter) Lint(node ast.Node, ctx *context.Context) types.Type {
 }
 
 func (l *Linter) lintUnusedTables(ctx *context.Context) {
-	for _, t := range ctx.Tables {
+	for key, t := range ctx.Tables {
 		if t.IsUsed {
 			continue
 		}
-		l.Error(UnusedDeclaration(t.Decl.GetMeta(), t.Name, "table").Match(UNUSED_DECLARATION))
+		if t.Decl == nil {
+			l.Error(UnusedExternalDeclaration(key, "table").Match(UNUSED_DECLARATION))
+		} else {
+			l.Error(UnusedDeclaration(t.Decl.GetMeta(), t.Name, "table").Match(UNUSED_DECLARATION))
+		}
 	}
 }
 
 func (l *Linter) lintUnusedAcls(ctx *context.Context) {
-	for _, a := range ctx.Acls {
+	for key, a := range ctx.Acls {
 		if a.IsUsed {
 			continue
 		}
-		l.Error(UnusedDeclaration(a.Decl.GetMeta(), a.Decl.Name.Value, "acl").Match(UNUSED_DECLARATION))
+		if a.Decl == nil {
+			l.Error(UnusedExternalDeclaration(key, "acl").Match(UNUSED_DECLARATION))
+		} else {
+			l.Error(UnusedDeclaration(a.Decl.GetMeta(), a.Decl.Name.Value, "acl").Match(UNUSED_DECLARATION))
+		}
 	}
 }
 
 func (l *Linter) lintUnusedBackends(ctx *context.Context) {
-	for _, b := range ctx.Backends {
+	for key, b := range ctx.Backends {
 		if b.IsUsed {
 			continue
 		}
 		if b.DirectorDecl != nil {
 			l.Error(UnusedDeclaration(b.DirectorDecl.GetMeta(), b.DirectorDecl.Name.Value, "director").Match(UNUSED_DECLARATION))
 		} else {
-			l.Error(UnusedDeclaration(b.BackendDecl.GetMeta(), b.BackendDecl.Name.Value, "backend").Match(UNUSED_DECLARATION))
+			if b.BackendDecl == nil {
+				l.Error(UnusedExternalDeclaration(key, "backend").Match(UNUSED_DECLARATION))
+			} else {
+				l.Error(UnusedDeclaration(b.BackendDecl.GetMeta(), b.BackendDecl.Name.Value, "backend").Match(UNUSED_DECLARATION))
+			}
 		}
 	}
 }
diff --git a/remote/fetcher.go b/remote/fetcher.go
new file mode 100644
index 0000000..9a54421
--- /dev/null
+++ b/remote/fetcher.go
@@ -0,0 +1,105 @@
+package remote
+
+import (
+	_context "context"
+	"fmt"
+	"net/http"
+	"sync"
+	"time"
+
+	"github.com/ysugimoto/falco/types"
+)
+
+type FastlyApiFetcher struct {
+	client *FastlyClient
+	// Version can be set by multiple functions
+	lock    sync.RWMutex
+	version int64
+	timeout time.Duration
+}
+
+func NewFastlyApiFetcher(serviceId, apiKey string, timeout time.Duration) *FastlyApiFetcher {
+	return &FastlyApiFetcher{
+		client:  NewFastlyClient(http.DefaultClient, serviceId, apiKey),
+		version: -1,
+		timeout: timeout,
+	}
+}
+
+func (f *FastlyApiFetcher) Backends() ([]*types.RemoteBackend, error) {
+	ctx, timeout := _context.WithTimeout(_context.Background(), f.timeout)
+	defer timeout()
+	version, err := f.getVersion(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get latest version %w", err)
+	}
+
+	fstlyBack, err := f.client.ListBackends(ctx, version)
+	if err != nil {
+		return nil, err
+	}
+	r := []*types.RemoteBackend{}
+	for _, b := range fstlyBack {
+		r = append(r, &types.RemoteBackend{
+			Name:   b.Name,
+			Shield: b.Shield,
+		})
+	}
+	return r, nil
+}
+func (f *FastlyApiFetcher) Dictionaries() ([]*types.RemoteDictionary, error) {
+	c, timeout := _context.WithTimeout(_context.Background(), f.timeout)
+	defer timeout()
+	version, err := f.getVersion(c)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get latest version %w", err)
+	}
+
+	fstlyDic, err := f.client.ListEdgeDictionaries(c, version)
+	if err != nil {
+		return nil, err
+	}
+
+	r := []*types.RemoteDictionary{}
+	for _, d := range fstlyDic {
+		r = append(r, &types.RemoteDictionary{
+			Name: d.Name,
+		})
+	}
+	return r, nil
+}
+func (f *FastlyApiFetcher) Acls() ([]*types.RemoteAcl, error) {
+	c, timeout := _context.WithTimeout(_context.Background(), f.timeout)
+	defer timeout()
+	version, err := f.getVersion(c)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get latest version %w", err)
+	}
+
+	fastlyAcls, err := f.client.ListAccessControlLists(c, version)
+	if err != nil {
+		return nil, err
+	}
+
+	r := []*types.RemoteAcl{}
+	for _, a := range fastlyAcls {
+		r = append(r, &types.RemoteAcl{
+			Name: a.Name,
+		})
+	}
+	return r, nil
+}
+
+func (f *FastlyApiFetcher) getVersion(c _context.Context) (int64, error) {
+	defer f.lock.Unlock()
+	f.lock.Lock()
+	if f.version != -1 {
+		return f.version, nil
+	}
+	v, err := f.client.LatestVersion(c)
+	if err != nil {
+		return -1, err
+	}
+	f.version = v
+	return v, nil
+}
diff --git a/terraform/data/terraform-duplicate.json b/terraform/data/terraform-duplicate.json
new file mode 100644
index 0000000..f79b799
--- /dev/null
+++ b/terraform/data/terraform-duplicate.json
@@ -0,0 +1,64 @@
+{
+    "planned_values": {
+        "root_module": {
+            "resources": [
+                {
+                    "provider_name": "registry.terraform.io/fastly/fastly",
+                    "type": "fastly_service_vcl",
+                    "values": {
+                        "acl": [
+                            {
+                                "acl_id": "this is another id",
+                                "force_destroy": false,
+                                "name": "foo_acl"
+                            }
+                        ],
+                        "backend": [
+                            {
+                            "address": "foo.com",
+                            "auto_loadbalance": false,
+                            "between_bytes_timeout": 10000,
+                            "connect_timeout": 1000,
+                            "error_threshold": 0,
+                            "first_byte_timeout": 15000,
+                            "healthcheck": "foo_check",
+                            "max_conn": 200,
+                            "max_tls_version": "",
+                            "min_tls_version": "1.2",
+                            "name": "foo_backend",
+                            "override_host": "",
+                            "port": 443,
+                            "request_condition": "",
+                            "shield": "",
+                            "ssl_ca_cert": "",
+                            "ssl_check_cert": true,
+                            "ssl_ciphers": "",
+                            "ssl_client_cert": "",
+                            "ssl_client_key": "",
+                            "ssl_hostname": "",
+                            "ssl_sni_hostname": "",
+                            "use_ssl": true,
+                            "weight": 100
+                            }
+                        ],
+                        "dictionary": [
+                            {
+                                "dictionary_id": "this is an id",
+                                "force_destroy": false,
+                                "name": "foo_dictionary",
+                                "write_only": false
+                            }
+                        ],
+                        "vcl": [
+                            {
+                                "content": "acl foo_acl{}\n sub vcl_recv { \n #FASTLY RECV \n if (req.http.foo ~ foo_acl && table.contains(foo_dictionary, \"foo\")){ \n set req.backend = F_foo_backend;\n}\n}",
+                                "main": true,
+                                "name": "main.vcl"
+                            }
+                        ]
+                    }
+                }
+            ]
+        }
+    }
+}
diff --git a/terraform/data/terraform-empty.json b/terraform/data/terraform-empty.json
new file mode 100644
index 0000000..155e3eb
--- /dev/null
+++ b/terraform/data/terraform-empty.json
@@ -0,0 +1,27 @@
+{
+    "planned_values": {
+        "root_module": {
+            "resources": [
+                {
+                    "provider_name": "registry.terraform.io/fastly/fastly",
+                    "type": "fastly_service_vcl",
+                    "values": {
+                        "acl": [
+                        ],
+                        "backend": [
+                        ],
+                        "dictionary": [
+                        ],
+                        "vcl": [
+                            {
+                                "content": "sub vcl_recv {\n#FASTLY RECV set req.http.foo = \"bar\"; \n}",
+                                "main": true,
+                                "name": "main.vcl"
+                            }
+                        ]
+                    }
+                }
+            ]
+        }
+    }
+}
diff --git a/terraform/data/terraform-invalid.json b/terraform/data/terraform-invalid.json
new file mode 100644
index 0000000..2b0e769
--- /dev/null
+++ b/terraform/data/terraform-invalid.json
@@ -0,0 +1,3 @@
+{
+    "invalid": "this is not a proper json"
+}
diff --git a/terraform/data/terraform-valid-shielded.json b/terraform/data/terraform-valid-shielded.json
new file mode 100644
index 0000000..51accd1
--- /dev/null
+++ b/terraform/data/terraform-valid-shielded.json
@@ -0,0 +1,64 @@
+{
+    "planned_values": {
+        "root_module": {
+            "resources": [
+                {
+                    "provider_name": "registry.terraform.io/fastly/fastly",
+                    "type": "fastly_service_vcl",
+                    "values": {
+                        "acl": [
+                            {
+                                "acl_id": "this is another id",
+                                "force_destroy": false,
+                                "name": "foo_acl"
+                            }
+                        ],
+                        "backend": [
+                            {
+                            "address": "foo.com",
+                            "auto_loadbalance": false,
+                            "between_bytes_timeout": 10000,
+                            "connect_timeout": 1000,
+                            "error_threshold": 0,
+                            "first_byte_timeout": 15000,
+                            "healthcheck": "foo_check",
+                            "max_conn": 200,
+                            "max_tls_version": "",
+                            "min_tls_version": "1.2",
+                            "name": "foo_backend",
+                            "override_host": "",
+                            "port": 443,
+                            "request_condition": "",
+                            "shield": "this-is-a-shield",
+                            "ssl_ca_cert": "",
+                            "ssl_check_cert": true,
+                            "ssl_ciphers": "",
+                            "ssl_client_cert": "",
+                            "ssl_client_key": "",
+                            "ssl_hostname": "",
+                            "ssl_sni_hostname": "",
+                            "use_ssl": true,
+                            "weight": 100
+                            }
+                        ],
+                        "dictionary": [
+                            {
+                                "dictionary_id": "this is an id",
+                                "force_destroy": false,
+                                "name": "foo_dictionary",
+                                "write_only": false
+                            }
+                        ],
+                        "vcl": [
+                            {
+                                "content": "sub vcl_recv { \n #FASTLY RECV \n if (req.http.foo ~ foo_acl && table.contains(foo_dictionary, \"foo\")){ \n set req.backend = ssl_shield_;\n}\n}",
+                                "main": true,
+                                "name": "main.vcl"
+                            }
+                        ]
+                    }
+                }
+            ]
+        }
+    }
+}
diff --git a/terraform/data/terraform-valid.json b/terraform/data/terraform-valid.json
new file mode 100644
index 0000000..d7f6754
--- /dev/null
+++ b/terraform/data/terraform-valid.json
@@ -0,0 +1,64 @@
+{
+    "planned_values": {
+        "root_module": {
+            "resources": [
+                {
+                    "provider_name": "registry.terraform.io/fastly/fastly",
+                    "type": "fastly_service_vcl",
+                    "values": {
+                        "acl": [
+                            {
+                                "acl_id": "this is another id",
+                                "force_destroy": false,
+                                "name": "foo_acl"
+                            }
+                        ],
+                        "backend": [
+                            {
+                            "address": "foo.com",
+                            "auto_loadbalance": false,
+                            "between_bytes_timeout": 10000,
+                            "connect_timeout": 1000,
+                            "error_threshold": 0,
+                            "first_byte_timeout": 15000,
+                            "healthcheck": "foo_check",
+                            "max_conn": 200,
+                            "max_tls_version": "",
+                            "min_tls_version": "1.2",
+                            "name": "foo_backend",
+                            "override_host": "",
+                            "port": 443,
+                            "request_condition": "",
+                            "shield": "",
+                            "ssl_ca_cert": "",
+                            "ssl_check_cert": true,
+                            "ssl_ciphers": "",
+                            "ssl_client_cert": "",
+                            "ssl_client_key": "",
+                            "ssl_hostname": "",
+                            "ssl_sni_hostname": "",
+                            "use_ssl": true,
+                            "weight": 100
+                            }
+                        ],
+                        "dictionary": [
+                            {
+                                "dictionary_id": "this is an id",
+                                "force_destroy": false,
+                                "name": "foo_dictionary",
+                                "write_only": false
+                            }
+                        ],
+                        "vcl": [
+                            {
+                                "content": "sub vcl_recv { \n #FASTLY RECV \n if (req.http.foo ~ foo_acl && table.contains(foo_dictionary, \"foo\")){ \n set req.backend = F_foo_backend;\n}\n}",
+                                "main": true,
+                                "name": "main.vcl"
+                            }
+                        ]
+                    }
+                }
+            ]
+        }
+    }
+}
diff --git a/terraform/fetcher.go b/terraform/fetcher.go
new file mode 100644
index 0000000..6f1b350
--- /dev/null
+++ b/terraform/fetcher.go
@@ -0,0 +1,52 @@
+package terraform
+
+import (
+	"github.com/ysugimoto/falco/types"
+)
+
+type TerraformFetcher struct {
+	services []*FastlyService
+}
+
+func NewTerraformFetcher(s []*FastlyService) *TerraformFetcher {
+	return &TerraformFetcher{
+		services: s,
+	}
+}
+
+func (f *TerraformFetcher) Backends() ([]*types.RemoteBackend, error) {
+	var b []*types.RemoteBackend
+	for _, s := range f.services {
+		for _, serviceBackend := range s.Backends {
+			b = append(b, &types.RemoteBackend{
+				Name:   serviceBackend.Name,
+				Shield: serviceBackend.Shield,
+			})
+		}
+	}
+	return b, nil
+}
+
+func (f *TerraformFetcher) Dictionaries() ([]*types.RemoteDictionary, error) {
+	var d []*types.RemoteDictionary
+	for _, s := range f.services {
+		for _, sDictionary := range s.Dictionaries {
+			d = append(d, &types.RemoteDictionary{
+				Name: sDictionary.Name,
+			})
+		}
+	}
+	return d, nil
+}
+
+func (f *TerraformFetcher) Acls() ([]*types.RemoteAcl, error) {
+	var a []*types.RemoteAcl
+	for _, s := range f.services {
+		for _, sACL := range s.Acls {
+			a = append(a, &types.RemoteAcl{
+				Name: sACL.Name,
+			})
+		}
+	}
+	return a, nil
+}
diff --git a/cmd/falco/terraform.go b/terraform/terraform.go
similarity index 68%
rename from cmd/falco/terraform.go
rename to terraform/terraform.go
index 8076727..c89e218 100644
--- a/cmd/falco/terraform.go
+++ b/terraform/terraform.go
@@ -1,4 +1,4 @@
-package main
+package terraform
 
 import (
 	"encoding/json"
@@ -20,17 +20,38 @@ type TerraformVcl struct {
 	Name    string
 }
 
-type FastlyService struct {
+type TerraformAcl struct {
 	Name string
-	Vcls []*TerraformVcl
+}
+
+type TerraformDictionary struct {
+	Name string
+}
+
+// TODO(davinci26): We can unmarshall all the properties from the TF file
+// and lint them to make sure they have sane values.
+type TerraformBackend struct {
+	Name   string
+	Shield *string
+}
+
+type FastlyService struct {
+	Name         string
+	Vcls         []*TerraformVcl
+	Backends     []*TerraformBackend
+	Acls         []*TerraformAcl
+	Dictionaries []*TerraformDictionary
 }
 
 type TerraformPlannedResource struct {
 	ProviderName string `json:"provider_name"`
 	Type         string `json:"type"`
 	Values       *struct {
-		Name string          `json:"name"`
-		Vcl  []*TerraformVcl `json:"vcl"`
+		Name       string                 `json:"name"`
+		Vcl        []*TerraformVcl        `json:"vcl"`
+		Acl        []*TerraformAcl        `json:"acl"`
+		Backend    []*TerraformBackend    `json:"backend"`
+		Dictionary []*TerraformDictionary `json:"dictionary"`
 	} `json:"values"`
 }
 
@@ -45,7 +66,7 @@ type TerraformPlannedInput struct {
 	} `json:"planned_values"`
 }
 
-func unmarshalTerraformPlannedInput(buf []byte) ([]*FastlyService, error) {
+func UnmarshalTerraformPlannedInput(buf []byte) ([]*FastlyService, error) {
 	var root TerraformPlannedInput
 
 	if err := json.Unmarshal(buf, &root); err != nil {
@@ -69,8 +90,11 @@ func unmarshalTerraformPlannedInput(buf []byte) ([]*FastlyService, error) {
 			}
 
 			services = append(services, &FastlyService{
-				Name: v.Values.Name,
-				Vcls: v.Values.Vcl,
+				Name:         v.Values.Name,
+				Vcls:         v.Values.Vcl,
+				Acls:         v.Values.Acl,
+				Backends:     v.Values.Backend,
+				Dictionaries: v.Values.Dictionary,
 			})
 		}
 	}
@@ -83,8 +107,11 @@ func unmarshalTerraformPlannedInput(buf []byte) ([]*FastlyService, error) {
 			}
 
 			services = append(services, &FastlyService{
-				Name: v.Values.Name,
-				Vcls: v.Values.Vcl,
+				Name:         v.Values.Name,
+				Vcls:         v.Values.Vcl,
+				Acls:         v.Values.Acl,
+				Backends:     v.Values.Backend,
+				Dictionaries: v.Values.Dictionary,
 			})
 		}
 	}
diff --git a/types/types.go b/types/types.go
index caa8c03..c53623d 100644
--- a/types/types.go
+++ b/types/types.go
@@ -268,3 +268,32 @@ type Goto struct {
 func (g *Goto) Type() Type         { return GotoType }
 func (g *Goto) Token() token.Token { return g.Decl.Token }
 func (g *Goto) String() string     { return g.Decl.String() }
+
+type AclEntry struct {
+	Ip      string
+	Negated string
+	Subnet  *int64
+	Comment string
+}
+
+type RemoteAcl struct {
+	Name    string
+	Entries []*AclEntry
+}
+
+type RemoteDictionaryItem struct {
+	Key   string
+	Value string
+}
+
+type RemoteDictionary struct {
+	Name  string
+	Items []*RemoteDictionaryItem
+}
+
+// TODO(davinci26): We can unmarshall all the properties from the TF file
+// and lint them to make sure they have sane values.
+type RemoteBackend struct {
+	Name   string
+	Shield *string
+}


Test patch:
diff --git a/cmd/falco/runner_test.go b/cmd/falco/runner_test.go
index 505a887..eceff23 100644
--- a/cmd/falco/runner_test.go
+++ b/cmd/falco/runner_test.go
@@ -2,7 +2,10 @@ package main
 
 import (
 	"errors"
+	"os"
 	"testing"
+
+	"github.com/ysugimoto/falco/terraform"
 )
 
 type mockResolver struct {
@@ -32,6 +35,85 @@ func (m *mockResolver) Name() string {
 	return ""
 }
 
+func loadFromTfJson(fileName string, t *testing.T) ([]Resolver, Fetcher) {
+	buf, err := os.ReadFile(fileName)
+	if err != nil {
+		t.Fatalf("Unexpected error %s reading file %s ", fileName, err)
+	}
+
+	services, err := terraform.UnmarshalTerraformPlannedInput(buf)
+	if err != nil {
+		t.Fatalf("Unexpected error %s unarshalling %s ", fileName, err)
+	}
+
+	rslv := NewTerraformResolver(services)
+	f := terraform.NewTerraformFetcher(services)
+	return rslv, f
+}
+
+func TestResolveExternalWithExternalProperties(t *testing.T) {
+	fileName := "../../terraform/data/terraform-valid.json"
+	rslv, f := loadFromTfJson(fileName, t)
+	r, err := NewRunner(rslv[0], &Config{V: true}, f)
+	if err != nil {
+		t.Fatalf("Unexpected runner creation error: %s", err)
+		return
+	}
+	ret, err := r.Run()
+	if err != nil {
+		t.Fatalf("Unexpected Run() error: %s", err)
+	}
+	if ret.Infos > 0 {
+		t.Errorf("Infos expects 0, got %d", ret.Infos)
+	}
+	if ret.Warnings != 0 {
+		t.Errorf("Warning expects 0, got %d", ret.Warnings)
+	}
+	if ret.Errors > 0 {
+		t.Errorf("Errors expects 0, got %d", ret.Errors)
+	}
+}
+
+func TestResolveExternalWithNoExternalProperties(t *testing.T) {
+	fileName := "../../terraform/data/terraform-empty.json"
+	rslv, f := loadFromTfJson(fileName, t)
+	r, err := NewRunner(rslv[0], &Config{V: true}, f)
+	if err != nil {
+		t.Fatalf("Unexpected runner creation error: %s", err)
+		return
+	}
+	ret, err := r.Run()
+	if err != nil {
+		t.Fatalf("Unexpected Run() error: %s", err)
+	}
+	if ret.Infos > 0 {
+		t.Errorf("Infos expects 0, got %d", ret.Infos)
+	}
+	if ret.Warnings != 0 {
+		t.Errorf("Warning expects 0, got %d", ret.Warnings)
+	}
+	if ret.Errors > 0 {
+		t.Errorf("Errors expects 0, got %d", ret.Errors)
+	}
+}
+
+func TestResolveWithDuplicateDeclarations(t *testing.T) {
+	fileName := "../../terraform/data/terraform-duplicate.json"
+	rslv, f := loadFromTfJson(fileName, t)
+	r, err := NewRunner(rslv[0], &Config{V: true}, f)
+	if err != nil {
+		t.Fatalf("Unexpected runner creation error: %s", err)
+	}
+	ret, err := r.Run()
+	if err != nil {
+		t.Fatalf("Unexpected Run() error: %s", err)
+	}
+
+	if ret.Errors != 1 {
+		t.Errorf("Errors expects 1, got %d", ret.Errors)
+	}
+}
+
 func TestResolveIncludeStatement(t *testing.T) {
 	mock := &mockResolver{
 		dependency: map[string]string{
@@ -71,7 +153,7 @@ sub vcl_recv {
 }
 		`,
 	}
-	r, err := NewRunner(mock, &Config{V: true})
+	r, err := NewRunner(mock, &Config{V: true}, nil)
 	if err != nil {
 		t.Errorf("Unexpected runner creation error: %s", err)
 		return
@@ -134,7 +216,7 @@ sub vcl_recv {
 }
 		`,
 	}
-	r, err := NewRunner(mock, &Config{V: true})
+	r, err := NewRunner(mock, &Config{V: true}, nil)
 	if err != nil {
 		t.Errorf("Unexpected runner creation error: %s", err)
 		return
@@ -206,7 +288,7 @@ func TestRepositoryExamples(t *testing.T) {
 				t.Errorf("Unexpected runner creation error: %s", err)
 				return
 			}
-			r, err := NewRunner(resolvers[0], &Config{V: true})
+			r, err := NewRunner(resolvers[0], &Config{V: true}, nil)
 			if err != nil {
 				t.Errorf("Unexpected runner creation error: %s", err)
 				return
diff --git a/linter/linter_test.go b/linter/linter_test.go
index 0e4b148..a024179 100644
--- a/linter/linter_test.go
+++ b/linter/linter_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/ysugimoto/falco/context"
 	"github.com/ysugimoto/falco/lexer"
 	"github.com/ysugimoto/falco/parser"
+	"github.com/ysugimoto/falco/types"
 )
 
 func assertNoError(t *testing.T, input string) {
@@ -1506,6 +1507,23 @@ acl foo {}
 			t.Errorf("Expect one lint error but empty returned")
 		}
 	})
+
+	t.Run("raise unused external error", func(t *testing.T) {
+		input := ` sub foo{}
+`
+		vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("unexpected parser error: %s", err)
+			t.FailNow()
+		}
+		ctx := context.New()
+		ctx.AddAcl("foo", &types.Acl{})
+		l := New()
+		l.Lint(vcl, ctx)
+		if len(l.Errors) == 0 {
+			t.Errorf("Expect one lint error but empty returned")
+		}
+	})
 }
 
 func TestUnusedTables(t *testing.T) {
@@ -1544,6 +1562,23 @@ table foo {}
 			t.Errorf("Expect one lint error but empty returned")
 		}
 	})
+
+	t.Run("raise unused external error", func(t *testing.T) {
+		input := ` sub foo{}
+`
+		vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("unexpected parser error: %s", err)
+			t.FailNow()
+		}
+		ctx := context.New()
+		ctx.AddTable("foo", &types.Table{})
+		l := New()
+		l.Lint(vcl, ctx)
+		if len(l.Errors) == 0 {
+			t.Errorf("Expect one lint error but empty returned")
+		}
+	})
 }
 
 func TestUnusedBackend(t *testing.T) {
@@ -1582,6 +1617,23 @@ backend foo {}
 			t.Errorf("Expect one lint error but empty returned")
 		}
 	})
+
+	t.Run("raise unused external error", func(t *testing.T) {
+		input := ` sub foo{}
+`
+		vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("unexpected parser error: %s", err)
+			t.FailNow()
+		}
+		ctx := context.New()
+		ctx.AddBackend("foo", &types.Backend{})
+		l := New()
+		l.Lint(vcl, ctx)
+		if len(l.Errors) == 0 {
+			t.Errorf("Expect one lint error but empty returned")
+		}
+	})
 }
 
 func TestUnusedSubroutine(t *testing.T) {
diff --git a/terraform/fetcher_test.go b/terraform/fetcher_test.go
new file mode 100644
index 0000000..e42b008
--- /dev/null
+++ b/terraform/fetcher_test.go
@@ -0,0 +1,50 @@
+package terraform
+
+import (
+	"os"
+	"testing"
+)
+
+func TestStuff(t *testing.T) {
+	for _, fileName := range []string{"./data/terraform-valid.json", "./data/terraform-valid-shielded.json"} {
+		buf, err := os.ReadFile(fileName)
+
+		if err != nil {
+			t.Fatalf("Unexpected error %s reading file %s ", fileName, err)
+		}
+
+		services, err := UnmarshalTerraformPlannedInput(buf)
+		if err != nil {
+			t.Fatalf("Unexpected error %s unarshalling %s ", fileName, err)
+		}
+
+		f := NewTerraformFetcher(services)
+
+		acls, _ := f.Acls()
+		if len(acls) != 1 {
+			t.Errorf("Length of ACLs should be %d, got %d", 1, len(acls))
+		}
+
+		if acls[0].Name != "foo_acl" {
+			t.Errorf("Acl name want %s, got %s", acls[0].Name, "foo_acl")
+		}
+
+		backends, _ := f.Backends()
+		if len(backends) != 1 {
+			t.Errorf("Length of Backends should be %d, got %d", 1, len(backends))
+		}
+
+		if backends[0].Name != "foo_backend" {
+			t.Errorf("Backend name want %s, got %s", backends[0].Name, "foo_backend")
+		}
+
+		dictionaries, _ := f.Dictionaries()
+		if len(dictionaries) != 1 {
+			t.Errorf("Length of dictionaries should be %d, got %d", 1, len(dictionaries))
+		}
+
+		if dictionaries[0].Name != "foo_dictionary" {
+			t.Errorf("Dictionary name want %s, got %s", dictionaries[0].Name, "foo_dictionary")
+		}
+	}
+}
diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go
new file mode 100644
index 0000000..f16699c
--- /dev/null
+++ b/terraform/terraform_test.go
@@ -0,0 +1,50 @@
+package terraform
+
+import (
+	"os"
+	"testing"
+)
+
+func TestUnmarshallValidTfJson(t *testing.T) {
+	fileName := "./data/terraform-valid.json"
+	buf, err := os.ReadFile(fileName)
+
+	if err != nil {
+		t.Fatalf("Unexpected error %s reading file %s ", fileName, err)
+	}
+
+	services, err := UnmarshalTerraformPlannedInput(buf)
+	if err != nil {
+		t.Fatalf("Unexpected error %s unarshalling %s ", fileName, err)
+	}
+
+	if len(services) != 1 {
+		t.Errorf("Length of services should be %d, got %d", 1, len(services))
+	}
+
+	if services[0].Acls[0].Name != "foo_acl" {
+		t.Errorf("Acl name want %s, got %s", services[0].Acls[0].Name, "foo_acl")
+	}
+
+	if services[0].Backends[0].Name != "foo_backend" {
+		t.Errorf("Backend name want %s, got %s", services[0].Backends[0].Name, "foo_backend")
+	}
+
+	if services[0].Dictionaries[0].Name != "foo_dictionary" {
+		t.Errorf("Dictionary name want %s, got %s", services[0].Dictionaries[0].Name, "foo_dictionary")
+	}
+}
+
+func TestUnmarshallInValidTfJson(t *testing.T) {
+	fileName := "./data/terraform-invalid.json"
+	buf, err := os.ReadFile(fileName)
+
+	if err != nil {
+		t.Fatalf("Unexpected error %s reading file %s ", fileName, err)
+	}
+
+	_, err = UnmarshalTerraformPlannedInput(buf)
+	if err == nil {
+		t.Fatalf("Expected error when unarshalling tf %s ", fileName)
+	}
+}