cos_image_analyzer: Milestone 3 - Kernel configs, Kernel Command Line, and Sysctl Settings

Change-Id: Ie6d02da610c1dcbead249ed818b3c1a0de4bdc09
diff --git a/src/cmd/cos_image_analyzer/internal/binary/CompressRootfs.txt b/src/cmd/cos_image_analyzer/internal/binary/CompressRootfs.txt
deleted file mode 100644
index 5cddf4a..0000000
--- a/src/cmd/cos_image_analyzer/internal/binary/CompressRootfs.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-/bin/
-/lib/modules/
-/lib64/
-/usr/libexec/
-/usr/bin/
-/usr/sbin/
-/usr/lib64/
-/usr/share/zoneinfo/
-/usr/share/git/
-/usr/lib/
-/sbin/
\ No newline at end of file
diff --git a/src/cmd/cos_image_analyzer/internal/binary/CompressStateful.txt b/src/cmd/cos_image_analyzer/internal/binary/CompressStateful.txt
deleted file mode 100644
index 38331fc..0000000
--- a/src/cmd/cos_image_analyzer/internal/binary/CompressStateful.txt
+++ /dev/null
@@ -1 +0,0 @@
-/var_overlay/db/
\ No newline at end of file
diff --git a/src/cmd/cos_image_analyzer/internal/binary/diff.go b/src/cmd/cos_image_analyzer/internal/binary/diff.go
index 504fdb1..73f6ce4 100644
--- a/src/cmd/cos_image_analyzer/internal/binary/diff.go
+++ b/src/cmd/cos_image_analyzer/internal/binary/diff.go
@@ -5,6 +5,7 @@
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"sort"
 	"strings"
 
 	"cos.googlesource.com/cos/tools/src/cmd/cos_image_analyzer/internal/input"
@@ -21,164 +22,232 @@
 	etcOSRelease = "/etc/os-release"
 )
 
-// Differences is a Intermediate Struct used to store all binary differences
+// Differences is a intermediate Struct used to store all binary differences
 // Field names are pre-defined in parse_input.go and will be cross-checked with -binary flag.
 type Differences struct {
 	Version            []string
 	BuildID            []string
 	Rootfs             string
-	KernelCommandLine  string
+	OSConfigs          map[string]string
 	Stateful           string
 	PartitionStructure string
-	SysctlSettings     string
-	OSConfigs          map[string]string
 	KernelConfigs      string
+	KernelCommandLine  map[string]string
+	SysctlSettings     string
 }
 
 // versionDiff calculates the Version difference of two images
-func (binaryDiff *Differences) versionDiff(image1, image2 *input.ImageInfo) {
+func (d *Differences) versionDiff(image1, image2 *input.ImageInfo) {
 	if image1.Version != image2.Version {
-		binaryDiff.Version = []string{image1.Version, image2.Version}
+		d.Version = []string{image1.Version, image2.Version}
 	}
 }
 
 // buildDiff calculates the BuildID difference of two images
-func (binaryDiff *Differences) buildDiff(image1, image2 *input.ImageInfo) {
+func (d *Differences) buildDiff(image1, image2 *input.ImageInfo) {
 	if image1.BuildID != image2.BuildID {
-		binaryDiff.BuildID = []string{image1.BuildID, image2.BuildID}
+		d.BuildID = []string{image1.BuildID, image2.BuildID}
 	}
 }
 
 // rootfsDiff calculates the Root FS difference of two images
-func (binaryDiff *Differences) rootfsDiff(image1, image2 *input.ImageInfo, flagInfo *input.FlagInfo) error {
-	compressedRoots, err := ioutil.ReadFile(flagInfo.CompressRootfsFile)
+func (d *Differences) rootfsDiff(image1, image2 *input.ImageInfo, flagInfo *input.FlagInfo) error {
+	rootfsDiff, err := directoryDiff(image1.RootfsPartition3, image2.RootfsPartition3, "rootfs", flagInfo.Verbose, flagInfo.CompressRootfsSlice)
 	if err != nil {
-		return fmt.Errorf("failed to convert file %v to slice: %v", flagInfo.CompressRootfsFile, err)
+		return fmt.Errorf("fail to diff Rootfs partitions %v and %v: %v", image1.RootfsPartition3, image2.RootfsPartition3, err)
 	}
-	compressedRootsSlice := strings.Split(string(compressedRoots), "\n")
-	rootfsDiff, err := directoryDiff(image1.RootfsPartition3, image2.RootfsPartition3, "rootfs", flagInfo.Verbose, compressedRootsSlice)
-	if err != nil {
-		return fmt.Errorf("fail to find rootfs difference: %v", err)
-	}
-	binaryDiff.Rootfs = rootfsDiff
+	d.Rootfs = rootfsDiff
 	return nil
 }
 
 // osConfigDiff calculates the OsConfig difference of two images
-func (binaryDiff *Differences) osConfigDiff(image1, image2 *input.ImageInfo, flagInfo *input.FlagInfo) error {
-	if err := findOSConfigs(image1, image2, binaryDiff); err != nil {
+func (d *Differences) osConfigDiff(image1, image2 *input.ImageInfo, flagInfo *input.FlagInfo) error {
+	mapOfEtcEntries, err := findOSConfigs(image1, image2) // Get map of /etc entries for both images
+	if err != nil {
 		return fmt.Errorf("failed to find OS Configs: %v", err)
 	}
-	for etcEntryName, diff := range binaryDiff.OSConfigs {
-		etcEntryPath := filepath.Join(etc, etcEntryName)
-		if diff != "" {
-			uniqueEntryPath := filepath.Join(diff+"/rootfs/", etcEntryPath)
-			info, err := os.Stat(uniqueEntryPath)
-			if err != nil {
-				return fmt.Errorf("failed to get info on file %v: %v", uniqueEntryPath, err)
+	output := make(map[string]string)
+	for etcEntryName, img := range mapOfEtcEntries {
+		etcEntryPath := filepath.Join(etc, etcEntryName) + "/"
+		if flagInfo.Verbose || !utilities.InArray(etcEntryPath, flagInfo.CompressRootfsSlice) { // Only diff if Verbose or etcEntry is not in CompressRootfs.txt
+			currentImage := img
+			if img != "" { // Unique /etc entry in Image 1 or Image2
+				output[etcEntryPath] += "Only in " + img + "/rootfs/etc: " + etcEntryName
+			} else { // Shared /etc entry in Image 1 and Image 2
+				osConfigDiff, err := pureDiff(filepath.Join(image1.RootfsPartition3, etcEntryPath), filepath.Join(image2.RootfsPartition3, etcEntryPath))
+				if err != nil {
+					return fmt.Errorf("fail to take \"diff -r --no-dereference\" on %v: %v", etcEntryPath, err)
+				}
+				currentImage = image1.TempDir
+				output[etcEntryPath] = osConfigDiff
 			}
 
-			if info.IsDir() {
-				binaryDiff.OSConfigs[etcEntryName] = diff + " has unique directory " + etcEntryPath
-			} else {
-				binaryDiff.OSConfigs[etcEntryName] = diff + " has unique file " + etcEntryPath
-			}
-		} else {
-			compressedRoots, err := ioutil.ReadFile(flagInfo.CompressRootfsFile)
+			fullPath := filepath.Join(currentImage, "/rootfs/", etcEntryPath)
+			entryFile, err := os.Stat(fullPath)
 			if err != nil {
-				return fmt.Errorf("failed to convert file %v to slice: %v", flagInfo.CompressRootfsFile, err)
+				return fmt.Errorf("failed to get info on file %v: %v", fullPath, err)
 			}
-			compressedRootsSlice := strings.Split(string(compressedRoots), "\n")
-
-			osConfigDiff, err := directoryDiff(filepath.Join(image1.RootfsPartition3, etcEntryPath), filepath.Join(image2.RootfsPartition3, etcEntryPath), "rootfs", flagInfo.Verbose, compressedRootsSlice)
-			if err != nil {
-				return fmt.Errorf("fail to find difference on /etc/ entry %v: %v", etcEntryName, err)
+			if output[etcEntryPath] != "" {
+				if entryFile.IsDir() {
+					output[etcEntryPath] = "Configs for directory " + etcEntryPath + "\n" + output[etcEntryPath]
+				} else {
+					output[etcEntryPath] = "Configs for file " + etcEntryPath + "\n" + output[etcEntryPath]
+				}
 			}
-			binaryDiff.OSConfigs[etcEntryName] = osConfigDiff
 		}
 	}
+	d.OSConfigs = output
 	return nil
 }
 
 // statefulDiff calculates the stateful partition difference of two images
-func (binaryDiff *Differences) statefulDiff(image1, image2 *input.ImageInfo, flagInfo *input.FlagInfo) error {
-	compressedStateful, err := ioutil.ReadFile(flagInfo.CompressStatefulFile)
+func (d *Differences) statefulDiff(image1, image2 *input.ImageInfo, flagInfo *input.FlagInfo) error {
+	statefulDiff, err := directoryDiff(image1.StatePartition1, image2.StatePartition1, "stateful", flagInfo.Verbose, flagInfo.CompressStatefulSlice)
 	if err != nil {
-		return fmt.Errorf("failed to convert file %v to slice: %v", flagInfo.CompressStatefulFile, err)
+		return fmt.Errorf("failed to diff stateful partitions %v and %v: %v", image1.StatePartition1, image2.StatePartition1, err)
 	}
-	compressedStatefulSlice := strings.Split(string(compressedStateful), "\n")
-
-	statefulDiff, err := directoryDiff(image1.StatePartition1, image2.StatePartition1, "stateful", flagInfo.Verbose, compressedStatefulSlice)
-	if err != nil {
-		return fmt.Errorf("failed to diff %v and %v: %v", image1.StatePartition1, image2.StatePartition1, err)
-	}
-	binaryDiff.Stateful = statefulDiff
+	d.Stateful = statefulDiff
 	return nil
 }
 
 // partitionStructureDiff calculates the Version difference of two images
-func (binaryDiff *Differences) partitionStructureDiff(image1, image2 *input.ImageInfo) error {
+func (d *Differences) partitionStructureDiff(image1, image2 *input.ImageInfo) error {
 	if image2.TempDir != "" {
 		partitionStructureDiff, err := pureDiff(image1.PartitionFile, image2.PartitionFile)
 		if err != nil {
-			return fmt.Errorf("fail to find Partition Structure difference: %v", err)
+			return fmt.Errorf("fail to compare both image's \"partitions.txt\" file: %v", err)
 		}
-		binaryDiff.PartitionStructure = partitionStructureDiff
+		d.PartitionStructure = partitionStructureDiff
 	} else {
 		image1Structure, err := ioutil.ReadFile(image1.PartitionFile)
 		if err != nil {
-			return fmt.Errorf("failed to convert file %v to string: %v", image1.PartitionFile, err)
+			return fmt.Errorf("failed to read partition file of image %v: %v", image1.TempDir, err)
 		}
-		binaryDiff.PartitionStructure = string(image1Structure)
+		d.PartitionStructure = string(image1Structure)
+	}
+	return nil
+}
+
+// kernelConfigsDiff calculates the kernel configs difference of two images
+func (d *Differences) kernelConfigsDiff(image1, image2 *input.ImageInfo) error {
+	if image2.TempDir != "" {
+		kernelConfigsDiff, err := pureDiff(image1.KernelConfigsFile, image2.KernelConfigsFile)
+		if err != nil {
+			return fmt.Errorf("fail to compare the two image's kernel configs files: %v", err)
+		}
+		d.KernelConfigs = kernelConfigsDiff
+	} else {
+		image1KernelConfigs, err := ioutil.ReadFile(image1.KernelConfigsFile)
+		if err != nil {
+			return fmt.Errorf("failed to read kernel configs file of image %v: %v", image1.TempDir, err)
+		}
+		d.KernelConfigs = string(image1KernelConfigs)
+	}
+	return nil
+}
+
+// kernelCommandLineDiff calculates the kernel commad line difference of two images
+func (d *Differences) kernelCommandLineDiff(image1, image2 *input.ImageInfo) error {
+	output := make(map[string]string)
+	if image2.TempDir != "" {
+		mapImage1 := getKclMap(strings.Fields(image1.KernelCommandLine))
+		mapImage2 := getKclMap(strings.Fields(image2.KernelCommandLine))
+
+		for key1, value1 := range mapImage1 {
+			if value2, ok := mapImage2[key1]; !ok { // Unique KCL parameter in image1
+				if value1 != "" {
+					output[key1] = "d\n" + "< " + key1 + "=" + value1
+				} else {
+					output[key1] = "d\n" + "< " + key1
+				}
+			} else if value2 != value1 { // Image1 and Image2 KCL parameter values differ
+				output[key1] = "c\n" + "< " + key1 + "=" + value1 + "\n---\n> " + key1 + "=" + value2
+			}
+		}
+		for key2, value2 := range mapImage2 {
+			if _, ok := mapImage1[key2]; !ok { // Unique KCL parameter in image2
+				if value2 != "" {
+					output[key2] = "a\n" + "> " + key2 + "=" + value2
+				} else {
+					output[key2] = "a\n" + "> " + key2
+				}
+			}
+		}
+	} else {
+		output["Image1 KCL"] = image1.KernelCommandLine
+	}
+	d.KernelCommandLine = output
+	return nil
+}
+
+// sysctlSettingsDiff calculates the sysctl Settings difference of two images
+func (d *Differences) sysctlSettingsDiff(image1, image2 *input.ImageInfo) error {
+	if image2.TempDir != "" {
+		sysctlSettingsDiff, err := pureDiff(image1.SysctlSettingsFile, image2.SysctlSettingsFile)
+		if err != nil {
+			return fmt.Errorf("fail to compare the two image's sysctl settings files: %v", err)
+		}
+		d.SysctlSettings = sysctlSettingsDiff
+	} else {
+		image1SysctlSettings, err := ioutil.ReadFile(image1.SysctlSettingsFile)
+		if err != nil {
+			return fmt.Errorf("failed to convert image 1's %v file to string: %v", image1.SysctlSettingsFile, err)
+		}
+		d.SysctlSettings = string(image1SysctlSettings)
 	}
 	return nil
 }
 
 // FormatVersionDiff returns a formated string of the version difference
-func (binaryDiff *Differences) FormatVersionDiff() string {
-	if len(binaryDiff.Version) == 2 {
-		if binaryDiff.Version[1] != "" {
-			return "-----Version-----\n< " + binaryDiff.Version[0] + "\n> " + binaryDiff.Version[1] + "\n\n"
+func (d *Differences) FormatVersionDiff() string {
+	if len(d.Version) == 2 {
+		if d.Version[1] != "" {
+			return "----------Version----------\n< " + d.Version[0] + "\n> " + d.Version[1] + "\n\n"
 		}
-		return "-----Version-----\n" + binaryDiff.Version[0] + "\n\n"
+		return "----------Version----------\n" + d.Version[0] + "\n\n"
 	}
 	return ""
 }
 
 // FormatBuildIDDiff returns a formated string of the build difference
-func (binaryDiff *Differences) FormatBuildIDDiff() string {
-	if len(binaryDiff.BuildID) == 2 {
-		if binaryDiff.BuildID[1] != "" {
-			return "-----BuildID-----\n< " + binaryDiff.BuildID[0] + "\n> " + binaryDiff.BuildID[1] + "\n\n"
+func (d *Differences) FormatBuildIDDiff() string {
+	if len(d.BuildID) == 2 {
+		if d.BuildID[1] != "" {
+			return "----------BuildID----------\n< " + d.BuildID[0] + "\n> " + d.BuildID[1] + "\n\n"
 		}
-		return "-----BuildID-----\n" + binaryDiff.BuildID[0] + "\n\n"
+		return "----------BuildID----------\n" + d.BuildID[0] + "\n\n"
 	}
 	return ""
 }
 
 // FormatRootfsDiff returns a formated string of the rootfs difference
-func (binaryDiff *Differences) FormatRootfsDiff() string {
-	if binaryDiff.Rootfs != "" {
-		return "-----RootFS-----\n " + binaryDiff.Rootfs + "\n\n"
+func (d *Differences) FormatRootfsDiff() string {
+	if d.Rootfs != "" {
+		return "----------RootFS----------\n" + d.Rootfs + "\n\n"
 	}
 	return ""
 }
 
 // FormatStatefulDiff returns a formated string of the stateful partition difference
-func (binaryDiff *Differences) FormatStatefulDiff() string {
-	if binaryDiff.Stateful != "" {
-		return "-----Stateful Partition-----\n " + binaryDiff.Stateful + "\n\n"
+func (d *Differences) FormatStatefulDiff() string {
+	if d.Stateful != "" {
+		return "----------Stateful Partition----------\n" + d.Stateful + "\n\n"
 	}
 	return ""
 }
 
 // FormatOSConfigDiff returns a formated string of the OS Config difference
-func (binaryDiff *Differences) FormatOSConfigDiff() string {
-	if len(binaryDiff.OSConfigs) > 0 {
-		osConfigDifference := "-----OS Configurations-----\n"
-		for etcEntryName, diff := range binaryDiff.OSConfigs {
-			if diff != "" {
-				osConfigDifference += etcEntryName + "\n" + diff + "\n\n"
+func (d *Differences) FormatOSConfigDiff() string {
+	if len(d.OSConfigs) > 0 {
+		osConfigDifference := "----------OS Configurations----------\n"
+		keys := make([]string, 0)
+		for k := range d.OSConfigs {
+			keys = append(keys, k)
+		}
+		sort.Strings(keys)
+		for _, k := range keys {
+			if d.OSConfigs[k] != "" {
+				osConfigDifference += d.OSConfigs[k] + "\n\n"
 			}
 		}
 		return osConfigDifference
@@ -187,9 +256,44 @@
 }
 
 // FormatPartitionStructureDiff returns a formated string of the partition structure difference
-func (binaryDiff *Differences) FormatPartitionStructureDiff() string {
-	if binaryDiff.PartitionStructure != "" {
-		return "-----Partition Structure-----\n " + binaryDiff.PartitionStructure + "\n\n"
+func (d *Differences) FormatPartitionStructureDiff() string {
+	if d.PartitionStructure != "" {
+		return "----------Partition Structure----------\n" + d.PartitionStructure + "\n\n"
+	}
+	return ""
+}
+
+// FormatKernelConfigsDiff returns a formated string of the kernel configs difference
+func (d *Differences) FormatKernelConfigsDiff() string {
+	if d.KernelConfigs != "" {
+		return "----------Kernel Configs----------\n" + d.KernelConfigs + "\n\n"
+	}
+	return ""
+}
+
+// FormatKernelCommandLineDiff returns a formated string of the KCL difference
+func (d *Differences) FormatKernelCommandLineDiff() string {
+	if len(d.KernelCommandLine) > 0 {
+		kclDifference := "----------Kernel Command Line----------\n"
+		keys := make([]string, 0)
+		for k := range d.KernelCommandLine {
+			keys = append(keys, k)
+		}
+		sort.Strings(keys)
+		for _, k := range keys {
+			if d.KernelCommandLine[k] != "" {
+				kclDifference += d.KernelCommandLine[k] + "\n\n"
+			}
+		}
+		return kclDifference
+	}
+	return ""
+}
+
+// FormatSysctlSettingsDiff returns a formated string of the Sysctrl settings difference
+func (d *Differences) FormatSysctlSettingsDiff() string {
+	if d.SysctlSettings != "" {
+		return "----------Sysctl settings----------\n" + d.SysctlSettings + "\n\n"
 	}
 	return ""
 }
@@ -211,11 +315,27 @@
 	if utilities.InArray("BuildID", flagInfo.BinaryTypesSelected) {
 		BinaryDiff.buildDiff(image1, image2)
 	}
+
 	if utilities.InArray("Partition-structure", flagInfo.BinaryTypesSelected) {
 		if err := BinaryDiff.partitionStructureDiff(image1, image2); err != nil {
 			return BinaryDiff, fmt.Errorf("Failed to get Partition-structure difference: %v", err)
 		}
 	}
+	if utilities.InArray("Kernel-configs", flagInfo.BinaryTypesSelected) {
+		if err := BinaryDiff.kernelConfigsDiff(image1, image2); err != nil {
+			return BinaryDiff, fmt.Errorf("failed to get Kernel-configs difference: %v", err)
+		}
+	}
+	if utilities.InArray("Kernel-command-line", flagInfo.BinaryTypesSelected) {
+		if err := BinaryDiff.kernelCommandLineDiff(image1, image2); err != nil {
+			return BinaryDiff, fmt.Errorf("failed to get Kernel-command-line difference: %v", err)
+		}
+	}
+	if utilities.InArray("Sysctl-settings", flagInfo.BinaryTypesSelected) {
+		if err := BinaryDiff.sysctlSettingsDiff(image1, image2); err != nil {
+			return BinaryDiff, fmt.Errorf("failed to get Sysctl-settings difference: %v", err)
+		}
+	}
 
 	if image2.TempDir != "" {
 		if utilities.InArray("Rootfs", flagInfo.BinaryTypesSelected) {
diff --git a/src/cmd/cos_image_analyzer/internal/binary/diff_test.go b/src/cmd/cos_image_analyzer/internal/binary/diff_test.go
index 299c88f..ab7a995 100644
--- a/src/cmd/cos_image_analyzer/internal/binary/diff_test.go
+++ b/src/cmd/cos_image_analyzer/internal/binary/diff_test.go
@@ -9,6 +9,9 @@
 
 // test Diff function
 func TestDiff(t *testing.T) {
+	testCompressRootfsSlice := []string{"/proc/", "/usr/lib/", "/util/", "/etc/os-release/", "/etc/sysctl.d/"}
+	testCompressStatefulSlice := []string{"/var_overlay/"}
+
 	// Rootfs test data
 	testVerboseRootfsDiff := `Files ../testdata/image1/rootfs/lib64/python.txt and ../testdata/image2/rootfs/lib64/python.txt differ
 Files ../testdata/image1/rootfs/proc/security/access.conf and ../testdata/image2/rootfs/proc/security/access.conf differ
@@ -22,17 +25,66 @@
 
 	// OS Config test data
 	testVerboseOSConfig := map[string]string{
-		"docker": `Files ../testdata/image1/rootfs/etc/docker/credentials.txt and ../testdata/image2/rootfs/etc/docker/credentials.txt differ
+		"/etc/docker/": `Configs for directory /etc/docker/
+diff -r --no-dereference ../testdata/image1/rootfs/etc/docker/credentials.txt ../testdata/image2/rootfs/etc/docker/credentials.txt
+1,2c1,2
+< Name: docker.10.2.4
+< job: makes micro kernels
+\ No newline at end of file
+---
+> Name: docker.10.2.1
+> job: makes macro kernels
+\ No newline at end of file
 Only in ../testdata/image1/rootfs/etc/docker/util: docker.txt
 Only in ../testdata/image1/rootfs/etc/docker/util: lib32
 Only in ../testdata/image2/rootfs/etc/docker/util: lib64`,
-		"os-release": "Files ../testdata/image1/rootfs/etc/os-release and ../testdata/image2/rootfs/etc/os-release differ"}
+		"/etc/os-release/": `Configs for file /etc/os-release/
+1c1
+< BUILD_ID=12871.119.0
+---
+> BUILD_ID=12371.273.0
+3c3
+< KERNEL_COMMIT_ID=fa84f12c6d738af9486e69a006a57df923f9476a
+---
+> KERNEL_COMMIT_ID=5d4ffd91281840f7a118143d77fbefb02e87943c
+5c5
+< VERSION_ID=81
+---
+> VERSION_ID=77
+8c8
+< VERSION=81
+---
+> VERSION=77`,
+		"/etc/sysctl.d/": `Configs for directory /etc/sysctl.d/
+diff -r --no-dereference ../testdata/image1/rootfs/etc/sysctl.d/00-sysctl.conf ../testdata/image2/rootfs/etc/sysctl.d/00-sysctl.conf
+8c8
+< net.ipv4.conf.all.rp_filter = 1
+---
+> net.ipv4.conf.all.rp_filter = 2
+11c11,14
+< net.ipv4.tcp_slow_start_after_idle = 0
+\ No newline at end of file
+---
+> net.ipv4.tcp_slow_start_after_idle = 1
+> 
+> # dumby variable
+> net.ipv4.conf = 2
+\ No newline at end of file`}
+
 	testBriefOSConfig := map[string]string{
-		"docker": `Files ../testdata/image1/rootfs/etc/docker/credentials.txt and ../testdata/image2/rootfs/etc/docker/credentials.txt differ
+		"/etc/docker/": `Configs for directory /etc/docker/
+diff -r --no-dereference ../testdata/image1/rootfs/etc/docker/credentials.txt ../testdata/image2/rootfs/etc/docker/credentials.txt
+1,2c1,2
+< Name: docker.10.2.4
+< job: makes micro kernels
+\ No newline at end of file
+---
+> Name: docker.10.2.1
+> job: makes macro kernels
+\ No newline at end of file
 Only in ../testdata/image1/rootfs/etc/docker/util: docker.txt
 Only in ../testdata/image1/rootfs/etc/docker/util: lib32
-Only in ../testdata/image2/rootfs/etc/docker/util: lib64`,
-		"os-release": "Files ../testdata/image1/rootfs/etc/os-release and ../testdata/image2/rootfs/etc/os-release differ"}
+Only in ../testdata/image2/rootfs/etc/docker/util: lib64`}
 
 	// Stateful test data
 	testVerboseStatefulDiff := `Only in ../testdata/image1/stateful/dev_image: image1_dev.txt
@@ -44,7 +96,7 @@
 Unique files in ../testdata/image1/stateful/var_overlay
 Unique files in ../testdata/image2/stateful/var_overlay`
 
-	// Partition Structure test data
+	// Partition Structure data
 	testPartitionStructure := `1c1
 < Disk /img_disks/cos_81_12871_119_disk/disk.raw: 20971520 sectors, 10.0 GiB
 ---
@@ -62,6 +114,86 @@
 ---
 >    8           86016          118783   16.0 MiB    0700  OEM`
 
+	// Kernel configs data
+	testKernelConfigsImage1 := `#
+# Compiler: Chromium OS 10.0_pre377782_p20200113-r10 clang version 10.0.0 (/var/cache/chromeos-cache/distfiles/host/egit-src/llvm-project 4e8231b5cf0f5f62c7a51a857e29f5be5cb55734)
+#
+CONFIG_GCC_VERSION=0
+CONFIG_CC_IS_CLANG=y
+CONFIG_CLANG_VERSION=100000
+
+#
+# General setup
+#
+CONFIG_INIT_ENV_ARG_LIMIT=32
+CONFIG_LOCALVERSION=""
+CONFIG_BUILD_SALT=""
+CONFIG_HAVE_KERNEL_GZIP=y`
+	testKernelConfigsDiff := `2c2
+< # Compiler: Chromium OS 10.0_pre377782_p20200113-r10 clang version 10.0.0 (/var/cache/chromeos-cache/distfiles/host/egit-src/llvm-project 4e8231b5cf0f5f62c7a51a857e29f5be5cb55734)
+---
+> # Compiler: Chromium OS 9.0_pre361749_p20190714-r4 clang version 9.0.0 (/var/cache/chromeos-cache/distfiles/host/egit-src/llvm-project c11de5eada2decd0a495ea02676b6f4838cd54fb) (based on LLVM 9.0.0svn)
+6c6
+< CONFIG_CLANG_VERSION=100000
+---
+> CONFIG_CLANG_VERSION=90000
+11a12
+> # CONFIG_COMPILE_TEST is not set
+12a14
+> # CONFIG_LOCALVERSION_AUTO is not set
+14c16
+< CONFIG_HAVE_KERNEL_GZIP=y
+\ No newline at end of file
+---
+> CONFIG_HAVE_KERNEL_GZIP=y`
+
+	// Kernel Command Line data
+	kclImage1 := `linux /syslinux/vmlinuz.A init=/usr/lib/systemd/systemd boot=local rootwait ro dm_verity.dev_wait=50`
+	kclImage2 := `linux /syslinux/vmlinuz.A init=/usr/lib32/systemd/ ro dm_verity.dev_wait=1      i915.modeset=1 cros_efi`
+	testKCLImage1 := map[string]string{"Image1 KCL": kclImage1}
+	testKCLDiff := map[string]string{
+		"boot": `d
+< boot=local`,
+		"cros_efi": `a
+> cros_efi`,
+		"dm_verity.dev_wait": `c
+< dm_verity.dev_wait=50
+---
+> dm_verity.dev_wait=1`,
+		"i915.modeset": `a
+> i915.modeset=1`,
+		"init": `c
+< init=/usr/lib/systemd/systemd
+---
+> init=/usr/lib32/systemd/`,
+		"rootwait": `d
+< rootwait`}
+
+	// Sysctl settings
+	testSysctlSettingsImage1 := `# /etc/sysctl.conf
+# Look in /proc/sys/ for all the things you can setup.
+#
+
+# Enables source route verification
+net.ipv4.conf.default.rp_filter = 1
+# Enable reverse path
+net.ipv4.conf.all.rp_filter = 1
+
+# Disable shrinking the cwnd when connection is idle
+net.ipv4.tcp_slow_start_after_idle = 0`
+	testSysctlSettingsDiff := `8c8
+< net.ipv4.conf.all.rp_filter = 1
+---
+> net.ipv4.conf.all.rp_filter = 2
+11c11,14
+< net.ipv4.tcp_slow_start_after_idle = 0
+\ No newline at end of file
+---
+> net.ipv4.tcp_slow_start_after_idle = 1
+> 
+> # dumby variable
+> net.ipv4.conf = 2
+\ No newline at end of file`
 	for _, tc := range []struct {
 		Image1   *input.ImageInfo
 		Image2   *input.ImageInfo
@@ -95,11 +227,11 @@
 			want:     &Differences{}},
 		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", RootfsPartition3: "../testdata/image1/rootfs/"},
 			Image2:   &input.ImageInfo{TempDir: "../testdata/image2", RootfsPartition3: "../testdata/image2/rootfs/"},
-			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Rootfs"}, Verbose: true, CompressRootfsFile: "../testdata/CompressRootfsFile.txt"},
+			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Rootfs"}, Verbose: true, CompressRootfsSlice: testCompressRootfsSlice},
 			want:     &Differences{Rootfs: testVerboseRootfsDiff}},
 		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", RootfsPartition3: "../testdata/image1/rootfs/"},
 			Image2:   &input.ImageInfo{TempDir: "../testdata/image2", RootfsPartition3: "../testdata/image2/rootfs/"},
-			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Rootfs"}, Verbose: false, CompressRootfsFile: "../testdata/CompressRootfsFile.txt"},
+			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Rootfs"}, Verbose: false, CompressRootfsSlice: testCompressRootfsSlice},
 			want:     &Differences{Rootfs: testBriefRootfsDiff}},
 
 		// OS Config difference test
@@ -109,36 +241,62 @@
 			want:     &Differences{}},
 		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", RootfsPartition3: "../testdata/image1/rootfs/"},
 			Image2:   &input.ImageInfo{TempDir: "../testdata/image2", RootfsPartition3: "../testdata/image2/rootfs/"},
-			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"OS-config"}, Verbose: true, CompressRootfsFile: "../testdata/CompressRootfsFile.txt"},
+			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"OS-config"}, Verbose: true, CompressRootfsSlice: testCompressRootfsSlice},
 			want:     &Differences{OSConfigs: testVerboseOSConfig}},
 		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", RootfsPartition3: "../testdata/image1/rootfs/"},
 			Image2:   &input.ImageInfo{TempDir: "../testdata/image2", RootfsPartition3: "../testdata/image2/rootfs/"},
-			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"OS-config"}, Verbose: false, CompressRootfsFile: "../testdata/CompressRootfsFile.txt"},
+			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"OS-config"}, Verbose: false, CompressRootfsSlice: testCompressRootfsSlice},
 			want:     &Differences{OSConfigs: testBriefOSConfig}},
 
 		// Stateful difference test
-		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", RootfsPartition3: "../testdata/image1/rootfs/"},
+		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", StatePartition1: "../testdata/image1/stateful/"},
 			Image2:   &input.ImageInfo{},
 			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Stateful-partition"}},
 			want:     &Differences{}},
 		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", StatePartition1: "../testdata/image1/stateful/"},
 			Image2:   &input.ImageInfo{TempDir: "../testdata/image2", StatePartition1: "../testdata/image2/stateful/"},
-			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Stateful-partition"}, Verbose: true, CompressStatefulFile: "../testdata/CompressStatefulFile.txt"},
+			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Stateful-partition"}, Verbose: true, CompressStatefulSlice: testCompressStatefulSlice},
 			want:     &Differences{Stateful: testVerboseStatefulDiff}},
 		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", StatePartition1: "../testdata/image1/stateful/"},
 			Image2:   &input.ImageInfo{TempDir: "../testdata/image2", StatePartition1: "../testdata/image2/stateful/"},
-			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Stateful-partition"}, Verbose: false, CompressStatefulFile: "../testdata/CompressStatefulFile.txt"},
+			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Stateful-partition"}, Verbose: false, CompressStatefulSlice: testCompressStatefulSlice},
 			want:     &Differences{Stateful: testBriefStatefulDiff}},
 
 		// Partition Structure
-		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", RootfsPartition3: "../testdata/image1/rootfs/"},
-			Image2:   &input.ImageInfo{},
-			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Partition-structure"}},
-			want:     &Differences{}},
 		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", PartitionFile: "../testdata/image1/partitions.txt"},
 			Image2:   &input.ImageInfo{TempDir: "../testdata/image2", PartitionFile: "../testdata/image2/partitions.txt"},
 			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Partition-structure"}},
 			want:     &Differences{PartitionStructure: testPartitionStructure}},
+
+		// Kernel Configs
+		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", KernelConfigsFile: "../testdata/image1/usr/src/linux-headers-4.19.112+/.config"},
+			Image2:   &input.ImageInfo{},
+			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Kernel-configs"}},
+			want:     &Differences{KernelConfigs: testKernelConfigsImage1}},
+		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", KernelConfigsFile: "../testdata/image1/usr/src/linux-headers-4.19.112+/.config"},
+			Image2:   &input.ImageInfo{TempDir: "../testdata/image2", KernelConfigsFile: "../testdata/image2/usr/src/linux-headers-4.19.112+/.config"},
+			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Kernel-configs"}},
+			want:     &Differences{KernelConfigs: testKernelConfigsDiff}},
+
+		// Kernel command line
+		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", KernelCommandLine: kclImage1},
+			Image2:   &input.ImageInfo{},
+			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Kernel-command-line"}},
+			want:     &Differences{KernelCommandLine: testKCLImage1}},
+		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", KernelCommandLine: kclImage1},
+			Image2:   &input.ImageInfo{TempDir: "../testdata/image2", KernelCommandLine: kclImage2},
+			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Kernel-command-line"}},
+			want:     &Differences{KernelCommandLine: testKCLDiff}},
+
+		// Sysctl settings
+		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", SysctlSettingsFile: "../testdata/image1/rootfs/etc/sysctl.d/00-sysctl.conf"},
+			Image2:   &input.ImageInfo{},
+			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Sysctl-settings"}},
+			want:     &Differences{SysctlSettings: testSysctlSettingsImage1}},
+		{Image1: &input.ImageInfo{TempDir: "../testdata/image1", SysctlSettingsFile: "../testdata/image1/rootfs/etc/sysctl.d/00-sysctl.conf"},
+			Image2:   &input.ImageInfo{TempDir: "../testdata/image2", SysctlSettingsFile: "../testdata/image2/rootfs/etc/sysctl.d/00-sysctl.conf"},
+			FlagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Sysctl-settings"}},
+			want:     &Differences{SysctlSettings: testSysctlSettingsDiff}},
 	} {
 		got, _ := Diff(tc.Image1, tc.Image2, tc.FlagInfo)
 
@@ -149,18 +307,29 @@
 			t.Fatalf("Diff expected BuildID %v, got: %v", tc.want.BuildID, got.BuildID)
 		}
 		if tc.want.Rootfs != got.Rootfs {
-			t.Fatalf("Diff expected Rootfs diff \n$%v$\ngot:\n$%v$", tc.want.Rootfs, got.Rootfs)
+			t.Fatalf("Diff expected Rootfs diff \n%v\ngot:\n%v", tc.want.Rootfs, got.Rootfs)
 		}
-		for etcEntry := range got.OSConfigs {
+		for etcEntry := range tc.want.OSConfigs {
 			if res, _ := utilities.CmpMapValues(tc.want.OSConfigs, got.OSConfigs, etcEntry); res != 0 {
-				t.Fatalf("Diff expected OSConfigs \n$%v$\ngot:\n$%v$", tc.want.OSConfigs, got.OSConfigs)
+				t.Fatalf("Diff expected OSConfigs \n%v\ngot:\n%v", tc.want.OSConfigs, got.OSConfigs)
 			}
 		}
 		if tc.want.Stateful != got.Stateful {
-			t.Fatalf("Diff expected stateful diff \n$%v$\ngot:\n$%v$", tc.want.Stateful, got.Stateful)
+			t.Fatalf("Diff expected stateful diff \n%v\ngot:\n%v", tc.want.Stateful, got.Stateful)
 		}
 		if tc.want.PartitionStructure != got.PartitionStructure {
 			t.Fatalf("Diff expected partition structure diff \n$%v$\ngot:\n$%v$", tc.want.PartitionStructure, got.PartitionStructure)
 		}
+		for kclParameter, diff := range tc.want.KernelCommandLine {
+			if diff != got.KernelCommandLine[kclParameter] {
+				t.Fatalf("Diff expected kernel command line \n$%v$\ngot:\n$%v$", tc.want.KernelCommandLine, got.KernelCommandLine)
+			}
+		}
+		if tc.want.KernelConfigs != got.KernelConfigs {
+			t.Fatalf("Diff expected kernel configs diff \n$%v$\ngot:\n$%v$", tc.want.KernelConfigs, got.KernelConfigs)
+		}
+		if tc.want.SysctlSettings != got.SysctlSettings {
+			t.Fatalf("Diff expected sysctl settings \n$%v$\ngot:\n$%v$", tc.want.SysctlSettings, got.SysctlSettings)
+		}
 	}
 }
diff --git a/src/cmd/cos_image_analyzer/internal/binary/helpers.go b/src/cmd/cos_image_analyzer/internal/binary/helpers.go
index 125f945..136c4d9 100644
--- a/src/cmd/cos_image_analyzer/internal/binary/helpers.go
+++ b/src/cmd/cos_image_analyzer/internal/binary/helpers.go
@@ -3,6 +3,7 @@
 import (
 	"fmt"
 	"io/ioutil"
+	"os"
 	"os/exec"
 	"path/filepath"
 	"regexp"
@@ -12,30 +13,30 @@
 	"cos.googlesource.com/cos/tools/src/cmd/cos_image_analyzer/internal/utilities"
 )
 
-// findOSConfigs finds the list of directory entries in the /etc of both images
-// and stores them into the binaryDiff struct
-/// Input:
-//   (*ImageInfo) image1 - Holds needed rootfs path for image 1
-//   (*ImageInfo) image2 - Holds needed rootfs path
-// Output:
-//   (*Differences) binaryDiff - Holds needed KernelCommandLine map
-func findOSConfigs(image1, image2 *input.ImageInfo, binaryDiff *Differences) error {
+// findOSConfigs creates a map of all /etc entries in both images
+// Format: {etcEntry: ""} if etcEntry is shared in both images
+//         {etcEntry: imageName} if etcEntry is unique to "imageName"
+func findOSConfigs(image1, image2 *input.ImageInfo) (map[string]string, error) {
 	etcFiles1, err := ioutil.ReadDir(image1.RootfsPartition3 + etc)
 	if err != nil {
-		return fmt.Errorf("fail to read contents of directory %v: %v", image1.RootfsPartition3+etc, err)
+		return map[string]string{}, fmt.Errorf("fail to read contents of directory %v: %v", image1.RootfsPartition3+etc, err)
 	}
 	etcEntries1 := []string{}
 	for _, f := range etcFiles1 {
-		etcEntries1 = append(etcEntries1, f.Name())
+		if _, err := os.Readlink(filepath.Join(image1.RootfsPartition3, etc, f.Name())); err != nil {
+			etcEntries1 = append(etcEntries1, f.Name())
+		}
 	}
 
 	etcFiles2, err := ioutil.ReadDir(image2.RootfsPartition3 + etc)
 	if err != nil {
-		return fmt.Errorf("fail to read contents of directory %v: %v", image2.RootfsPartition3+etc, err)
+		return map[string]string{}, fmt.Errorf("fail to read contents of directory %v: %v", image2.RootfsPartition3+etc, err)
 	}
 	etcEntries2 := []string{}
 	for _, f := range etcFiles2 {
-		etcEntries2 = append(etcEntries2, f.Name())
+		if _, err := os.Readlink(filepath.Join(image1.RootfsPartition3, etc, f.Name())); err != nil {
+			etcEntries2 = append(etcEntries2, f.Name())
+		}
 	}
 
 	osConfigsMap := make(map[string]string)
@@ -51,8 +52,29 @@
 			osConfigsMap[elem2] = image2.TempDir
 		}
 	}
-	binaryDiff.OSConfigs = osConfigsMap
-	return nil
+	return osConfigsMap, nil
+}
+
+// getKclMap converts a kernel commad line tokenized slice into a map where the keys are
+// kernel Command line parameters and the values are the parameter's value (if it exists)
+// Format: {kclParameter: value} if kclparameter follows form "param=value"
+//         {kclParameter: ""}if kclparameter follows form "param"
+func getKclMap(input []string) map[string]string {
+	output := make(map[string]string)
+	for _, elem := range input {
+		if strings.Contains(elem, "=") { // KCl parameter follows form "parameter=value"
+			if startOfEquals := strings.Index(elem, "="); startOfEquals >= 0 {
+				key, value := elem[:startOfEquals], ""
+				if startOfEquals != len(elem)-1 {
+					value = elem[startOfEquals+1:]
+				}
+				output[key] = value
+			}
+		} else { // KCl parameter follows form "parameter"
+			output[elem] = ""
+		}
+	}
+	return output
 }
 
 // findDiffDir finds the directory name from the "diff" command
@@ -61,9 +83,9 @@
 //   (string) line - A single line of output from the "diff -rq" command
 //   (string) dir1 - Path to directory 1
 //   (string) dir2 - Path to directory 2
-//   (bool) ok - Flag to indicate a directory has been found
 // Output:
 //   (string) dir1 or dir2 - The directory found in "line"
+//   (bool) ok - Flag to indicate a directory has been found
 func findDiffDir(line, dir1, dir2 string) (string, bool) {
 	lineSplit := strings.Split(line, " ")
 	if len(lineSplit) < 3 {
@@ -163,14 +185,20 @@
 // Output:
 //   (string) diff - The file difference output of the "diff" command
 func directoryDiff(dir1, dir2, root string, verbose bool, compressedDirs []string) (string, error) {
-	diff, err := exec.Command("sudo", "diff", "--no-dereference", "-rq", "-x", "etc", dir1, dir2).Output()
+	var cmd *exec.Cmd
+	if root == "rootfs" { // Only exclude "/etc" for Rootfs difference
+		cmd = exec.Command("sudo", "diff", "--no-dereference", "-rq", "-x", "etc", dir1, dir2)
+	} else {
+		cmd = exec.Command("sudo", "diff", "--no-dereference", "-rq", dir1, dir2)
+	}
+	diff, err := cmd.Output()
 	if exitError, ok := err.(*exec.ExitError); ok {
 		if exitError.ExitCode() == 2 {
 			return "", fmt.Errorf("failed to call 'diff' command on directories %v and %v: %v", dir1, dir2, err)
 		}
 	}
-	diffStrln := fmt.Sprintf("%s", diff)
-	diffStr := strings.TrimSuffix(diffStrln, "\n")
+
+	diffStr := strings.TrimSuffix(string(diff), "\n")
 	if verbose {
 		return diffStr, nil
 	}
@@ -183,13 +211,11 @@
 
 // pureDiff returns the output of a normal diff between two files or directories
 func pureDiff(input1, input2 string) (string, error) {
-	diff, err := exec.Command("sudo", "diff", input1, input2).Output()
+	diff, err := exec.Command("sudo", "diff", "-r", "--no-dereference", input1, input2).Output()
 	if exitError, ok := err.(*exec.ExitError); ok {
 		if exitError.ExitCode() == 2 {
 			return "", fmt.Errorf("failed to call 'diff' on %v and %v: %v", input1, input2, err)
 		}
 	}
-	diffStrln := fmt.Sprintf("%s", diff)
-	diffStr := strings.TrimSuffix(diffStrln, "\n")
-	return diffStr, nil
+	return strings.TrimSuffix(string(diff), "\n"), nil
 }
diff --git a/src/cmd/cos_image_analyzer/internal/binary/helpers_test.go b/src/cmd/cos_image_analyzer/internal/binary/helpers_test.go
index d07af4d..da5bbea 100644
--- a/src/cmd/cos_image_analyzer/internal/binary/helpers_test.go
+++ b/src/cmd/cos_image_analyzer/internal/binary/helpers_test.go
@@ -29,7 +29,7 @@
 	} {
 		got, _ := directoryDiff(tc.dir1, tc.dir2, tc.root, tc.verbose, tc.compressedDirs)
 		if got != tc.want {
-			t.Fatalf("directoryDiff expected:\n$%v$\ngot:\n$%v$", tc.want, got)
+			t.Fatalf("directoryDiff expected:\n%v\ngot:\n%v", tc.want, got)
 		}
 	}
 }
@@ -55,7 +55,32 @@
 	} {
 		got, _ := pureDiff(tc.input1, tc.input2)
 		if got != tc.want {
-			t.Fatalf("PureDiff expected:\n$%v$\ngot:\n$%v$", tc.want, got)
+			t.Fatalf("PureDiff expected:\n%v\ngot:\n%v", tc.want, got)
+		}
+	}
+}
+
+// test getKclMap function
+func TestGetKclMap(t *testing.T) {
+
+	for _, tc := range []struct {
+		input []string
+		want  map[string]string
+	}{
+		{input: []string{"linux", "/syslinux/vmlinuz.A", "init=/usr/lib/systemd/systemd", "boot=local", "rootwait", "ro", "dm_verity.dev_wait=50"},
+			want: map[string]string{"syslinux/vmlinuz.A": "", "boot": "local", "dm_verity.dev_wait": "50", "init": "/usr/lib/systemd/systemd", "linux": "", "ro": "", "rootwait": ""}},
+
+		{input: []string{"linux", "/syslinux/vmlinuz.A", "init=/usr/lib32/systemd/", "ro", "dm_verity.dev_wait=2", "i915.modeset=1", "cros_efi"},
+			want: map[string]string{"syslinux/vmlinuz.A": "", "dm_verity.dev_wait": "2", "init": "/usr/lib32/systemd/", "linux": "", "ro": "", "i915.modeset": "1"}},
+
+		{input: []string{},
+			want: map[string]string{}},
+	} {
+		got := getKclMap(tc.input)
+		for param, value := range tc.want {
+			if value != got[param] {
+				t.Fatalf("getKclMap expected:\n%v\ngot:\n%v", tc.want, got)
+			}
 		}
 	}
 }
diff --git a/src/cmd/cos_image_analyzer/internal/binary/info.go b/src/cmd/cos_image_analyzer/internal/binary/info.go
index cce8b5e..c0d21c2 100644
--- a/src/cmd/cos_image_analyzer/internal/binary/info.go
+++ b/src/cmd/cos_image_analyzer/internal/binary/info.go
@@ -1,25 +1,112 @@
 package binary
 
 import (
+	"bufio"
 	"errors"
 	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
 
 	"cos.googlesource.com/cos/tools/src/cmd/cos_image_analyzer/internal/input"
 	"cos.googlesource.com/cos/tools/src/cmd/cos_image_analyzer/internal/utilities"
 )
 
+const cosGCSBucket = "cos-tools"
+const kernelHeaderGCSObject = "kernel-headers.tgz"
+const pathToKernelConfigs = "usr/src/linux-headers-4.19.112+/.config"
+
+const pathToKernelCommandLine = "efi/boot/grub.cfg" // Located in partition 12 EFI
+const kclImageName = "verified image A"
+const startOfHashingKCL = "dm="
+
+const pathToSysctlSettings = "/etc/sysctl.d/00-sysctl.conf" // Located in partition 3 Root-A
+
+// getPartitionStructure returns the partition structure of .raw file
+func getPartitionStructure(image *input.ImageInfo) error {
+	if image.TempDir == "" {
+		return nil
+	}
+
+	out, err := exec.Command("sudo", "sgdisk", "-p", image.DiskFile).Output()
+	if err != nil {
+		return fmt.Errorf("failed to call sgdisk -p %v: %v", image.DiskFile, err)
+	}
+
+	partitionFile := filepath.Join(image.TempDir, "partitions.txt")
+	if err := utilities.WriteToNewFile(partitionFile, string(out[:])); err != nil {
+		return fmt.Errorf("failed create file %v and write %v: %v", partitionFile, string(out[:]), err)
+	}
+	image.PartitionFile = partitionFile
+	return nil
+}
+
+// getKernelConfigs downloads the kernel configs for a build from GCS and stores
+// it into the image's temporary directory
+func getKernelConfigs(image *input.ImageInfo) error {
+	gcsObject := filepath.Join(image.BuildID, kernelHeaderGCSObject)
+	tarFile, err := utilities.GcsDowndload(cosGCSBucket, gcsObject, image.TempDir, kernelHeaderGCSObject)
+	if err != nil {
+		return fmt.Errorf("failed to download GCS object %v from bucket %v: %v", gcsObject, cosGCSBucket, err)
+	}
+
+	_, err = exec.Command("tar", "-xf", tarFile, "-C", image.TempDir).Output()
+	if err != nil {
+		return fmt.Errorf("failed to unzip %v into %v: %v", tarFile, image.TempDir, err)
+	}
+	image.KernelConfigsFile = filepath.Join(image.TempDir, pathToKernelConfigs)
+	return nil
+}
+
+// getKernelCommandLine gets the kernel command line from the image's partition 12 EFI
+// located in the /efi/boot/grub.cfg file
+func getKernelCommandLine(image *input.ImageInfo) error {
+	kclPath := filepath.Join(image.EFIPartition12, pathToKernelCommandLine)
+	kclFile, err := os.Open(kclPath)
+	if err != nil {
+		return fmt.Errorf("Failed to open file %v: %v", kclPath, err)
+	}
+	defer kclFile.Close()
+
+	foundKCL := false
+	scanner := bufio.NewScanner(kclFile)
+	for scanner.Scan() { // Scan file line by line for "verified Image A"
+		kcl := string(scanner.Text()[:])
+
+		if foundKCL {
+			if hashStart := strings.Index(kcl, startOfHashingKCL); hashStart >= 0 {
+				kcl = kcl[:hashStart] // Remove hash "dm='....'" from kcl
+			}
+			image.KernelCommandLine = strings.TrimSpace(kcl)
+			return nil
+		}
+		if strings.Contains(kcl, kclImageName) {
+			foundKCL = true
+		}
+	}
+
+	if scanner.Err() != nil {
+		return fmt.Errorf("Failed to scan file %v: %v", kclPath, scanner.Err())
+	}
+	return nil
+}
+
+// getSysctlSettings finds an image's Sysctrl settings file under
+// the /etc/sysctrl.d/00-sysctl.conf
+func getSysctlSettings(image *input.ImageInfo) error {
+	sysctlPath := filepath.Join(image.RootfsPartition3, pathToSysctlSettings)
+	image.SysctlSettingsFile = sysctlPath
+	return nil
+}
+
 // GetBinaryInfo finds relevant binary information for the COS image
-// Input:
-//   (*ImageInfo) image - A struct that will store binary info for the image
-//   (localInput) bool - Flag to determine whether to rename disk.raw file
-//   (*FlagInfo) flagInfo - A struct that holds input preference from the user
-// Output: nil on success, else error
 func GetBinaryInfo(image *input.ImageInfo, flagInfo *input.FlagInfo) error {
 	if image.TempDir == "" {
 		return nil
 	}
 
-	if image.RootfsPartition3 != "" {
+	if image.RootfsPartition3 != "" { // Get Version and BuildID
 		osReleaseMap, err := utilities.ReadFileToMap(image.RootfsPartition3+etcOSRelease, "=")
 		if err != nil {
 			return fmt.Errorf("Failed to read /etc/os-release file in rootfs of image %v : %v", image.TempDir, err)
@@ -33,11 +120,28 @@
 		}
 	}
 
-	if utilities.InArray("Partition-structure", flagInfo.BinaryTypesSelected) {
-		if err := image.GetPartitionStructure(); err != nil {
+	if utilities.InArray("Partition-structure", flagInfo.BinaryTypesSelected) { // Get partition structure from "sgdisk -p"
+		if err := getPartitionStructure(image); err != nil {
 			return fmt.Errorf("failed to get partition structure for image %v: %v", image.TempDir, err)
 		}
 	}
 
+	if utilities.InArray("Kernel-configs", flagInfo.BinaryTypesSelected) { // Get kernel configs from gs://cos-tools/BuildID/kernel-headers.tgz
+		if err := getKernelConfigs(image); err != nil {
+			return fmt.Errorf("failed to get kernel configs for image %v: %v", image.TempDir, err)
+		}
+	}
+
+	if utilities.InArray("Kernel-command-line", flagInfo.BinaryTypesSelected) { // Get kernel command line from partition 12 EFI (efi/boot/grub.cfg)
+		if err := getKernelCommandLine(image); err != nil {
+			return fmt.Errorf("failed to get the kernel command line for image %v: %v", image.TempDir, err)
+		}
+	}
+
+	if utilities.InArray("Sysctl-settings", flagInfo.BinaryTypesSelected) { // Get Sysctl settings from /etc/sysctl.d/00-sysctl.conf
+		if err := getSysctlSettings(image); err != nil {
+			return fmt.Errorf("failed to get Sysctl-settings for image %v: %v", image.TempDir, err)
+		}
+	}
 	return nil
 }
diff --git a/src/cmd/cos_image_analyzer/internal/binary/info_test.go b/src/cmd/cos_image_analyzer/internal/binary/info_test.go
index 890444f..e7ae5e0 100644
--- a/src/cmd/cos_image_analyzer/internal/binary/info_test.go
+++ b/src/cmd/cos_image_analyzer/internal/binary/info_test.go
@@ -8,22 +8,26 @@
 
 // test GetBinaryInf function
 func TestGetBinaryInfo(t *testing.T) {
+	kclImage1 := `linux /syslinux/vmlinuz.A init=/usr/lib/systemd/systemd boot=local rootwait ro noresume noswap loglevel=7 noinitrd console=ttyS0 security=apparmor virtio_net.napi_tx=1 systemd.unified_cgroup_hierarchy=false systemd.legacy_systemd_cgroup_controller=false csm.disabled=1  dm_verity.error_behavior=3 dm_verity.max_bios=-1 dm_verity.dev_wait=1       i915.modeset=1 cros_efi root=/dev/dm-0`
 	for _, tc := range []struct {
 		image    *input.ImageInfo
 		flagInfo *input.FlagInfo
 		want     *input.ImageInfo
-	}{
+	}{ // Version and BuildID
 		{image: &input.ImageInfo{TempDir: "../testdata/image1/", RootfsPartition3: "../testdata/image1/rootfs/"},
 			flagInfo: &input.FlagInfo{LocalPtr: true},
 			want:     &input.ImageInfo{RootfsPartition3: "../testdata/image1", Version: "81", BuildID: "12871.119.0"}},
-
 		{image: &input.ImageInfo{},
 			flagInfo: &input.FlagInfo{LocalPtr: true},
 			want:     &input.ImageInfo{}},
-
 		{image: &input.ImageInfo{TempDir: "../testdata/image2/", RootfsPartition3: "../testdata/image2/rootfs/"},
 			flagInfo: &input.FlagInfo{LocalPtr: true},
 			want:     &input.ImageInfo{RootfsPartition3: "../testdata/image2/rootfs", Version: "77", BuildID: "12371.273.0"}},
+
+		// Kernel Command Line
+		{image: &input.ImageInfo{TempDir: "../testdata/image1", EFIPartition12: "../testdata/image1/efi/"},
+			flagInfo: &input.FlagInfo{BinaryTypesSelected: []string{"Kernel-command-line"}},
+			want:     &input.ImageInfo{TempDir: "../testdata/image1", EFIPartition12: "../testdata/image1/efi/", KernelCommandLine: kclImage1}},
 	} {
 		GetBinaryInfo(tc.image, tc.flagInfo)
 
@@ -33,5 +37,8 @@
 		if tc.want.BuildID != tc.image.BuildID {
 			t.Fatalf("GetBinaryInfo expected: %v, got: %v", tc.want.BuildID, tc.image.BuildID)
 		}
+		if tc.image.KernelCommandLine != tc.want.KernelCommandLine {
+			t.Fatalf("Diff kernel command line expected:$%v$, got:$%v$", tc.want.KernelCommandLine, tc.image.KernelCommandLine)
+		}
 	}
 }
diff --git a/src/cmd/cos_image_analyzer/internal/input/flaginfo.go b/src/cmd/cos_image_analyzer/internal/input/flaginfo.go
index 7370c4e..3f50d74 100644
--- a/src/cmd/cos_image_analyzer/internal/input/flaginfo.go
+++ b/src/cmd/cos_image_analyzer/internal/input/flaginfo.go
@@ -24,16 +24,25 @@
 	// Release Notes
 	ReleaseNotesSelected bool
 
-	//Verbosity of output
+	// Verbosity of output
+	// If true, full Rootfs, Os-Config, and Stateful Partition output is shown.
+	// Else false (default), Rootfs and Stateful Partition directorties listed on files
+	// 	pointed to by CompressRootfsFile and CompressStatefulFile respectively are compressed.
+	// 	For OS-configs difference, all /etc entries that are listed in CompressRootfsFile are ignored.
 	Verbose bool
 
-	// File used to compress output from Rootfs and OS-Config difference
+	// File used to compress directories in the output from Rootfs difference and
+	// for ignore entries under /etc for OS-Config difference
 	// (either user provided or default CompressRootfs.txt)
 	CompressRootfsFile string
+	// Slice of CompressRootfsFile
+	CompressRootfsSlice []string
 
-	// File used to compress output from Stateful-partition difference
+	// File used to compress directories in the output from Stateful-partition difference
 	// (either user provided or default CompressStateful.txt)
 	CompressStatefulFile string
+	// Slice of CompressRootfsFile
+	CompressStatefulSlice []string
 
 	// Output
 	OutputSelected string
diff --git a/src/cmd/cos_image_analyzer/internal/input/imageinfo.go b/src/cmd/cos_image_analyzer/internal/input/imageinfo.go
index 38089bb..8f75fd5 100644
--- a/src/cmd/cos_image_analyzer/internal/input/imageinfo.go
+++ b/src/cmd/cos_image_analyzer/internal/input/imageinfo.go
@@ -5,32 +5,30 @@
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io"
 	"io/ioutil"
 	"log"
 	"net/http"
 	"os"
 	"os/exec"
 	"path/filepath"
-	"strconv"
 	"strings"
 
 	"cos.googlesource.com/cos/tools/src/cmd/cos_image_analyzer/internal/utilities"
 )
 
-const sectorSize = 512
 const gcsObjFormat = ".tar.gz"
 const makeDirFilemode = 0700
 const timeOut = "7200s"
 const imageFormat = "vmdk"
 const name = "gcr.io/compute-image-tools/gce_vm_image_export:release"
+const pathToKernelConfigs = "usr/src/linux-headers-4.19.112+/.config"
+const pathToSysctlSettings = "/etc/sysctl.d/00-sysctl.conf" // Located in partition 3 Root-A
 
 // ImageInfo stores all relevant information on a COS image
 type ImageInfo struct {
 	// Input Overhead
 	TempDir          string // Temporary directory holding the mounted image and disk file
 	DiskFile         string // Path to the DOS/MBR disk partition file
-	PartitionFile    string // Path to the file storing the disk partition structure from "sgdisk"
 	StatePartition1  string // Path to mounted directory of partition #1, stateful partition
 	RootfsPartition3 string // Path to mounted directory of partition #3, Rootfs-A
 	EFIPartition12   string // Path to mounted directory of partition #12, EFI-System
@@ -39,12 +37,12 @@
 	LoopDevice12     string // Active loop device for mounted image
 
 	// Binary info
-	Version string
-	BuildID string
-
-	// Package info
-	// Commit info
-	// Release notes info
+	Version            string // Major cos version
+	BuildID            string // Minor cos version
+	PartitionFile      string // Path to the file storing the disk partition structure from "sgdisk"
+	SysctlSettingsFile string // Path to the /etc/sysctrl.d/00-sysctl.conf file of an image
+	KernelCommandLine  string // The kernel command line boot-time parameters stored in partition 12 efi/boot/grub.cfg
+	KernelConfigsFile  string // Path to the ".config" file downloaded from GCS that holds a build's kernel configs
 }
 
 // Rename temporary directory and its contents once Version and BuildID are known
@@ -52,113 +50,29 @@
 	if image.Version != "" && image.BuildID != "" {
 		fullImageName := "cos-" + image.Version + "-" + image.BuildID
 		if err := os.Rename(image.TempDir, fullImageName); err != nil {
-			return fmt.Errorf("Error: Failed to rename directory %v to %v: %v", image.TempDir, fullImageName, err)
+			return fmt.Errorf("failed to rename directory %v to %v: %v", image.TempDir, fullImageName, err)
 		}
 		image.TempDir = fullImageName
 
 		if !flagInfo.LocalPtr {
 			image.DiskFile = filepath.Join(fullImageName, "disk.raw")
 		}
-		if image.RootfsPartition3 != "" {
-			image.RootfsPartition3 = filepath.Join(fullImageName, "rootfs")
-		}
 		if image.StatePartition1 != "" {
 			image.StatePartition1 = filepath.Join(fullImageName, "stateful")
 		}
+		if image.RootfsPartition3 != "" {
+			image.RootfsPartition3 = filepath.Join(fullImageName, "rootfs")
+		}
 		if image.EFIPartition12 != "" {
 			image.EFIPartition12 = filepath.Join(fullImageName, "efi")
 		}
+		image.PartitionFile = filepath.Join(fullImageName, "partitions.txt")
+		image.KernelConfigsFile = filepath.Join(fullImageName, pathToKernelConfigs)
+		image.SysctlSettingsFile = filepath.Join(image.RootfsPartition3, pathToSysctlSettings)
 	}
 	return nil
 }
 
-// getPartitionStart finds the start partition offset of the disk
-// Input:
-//   (string) diskFile - Name of DOS/MBR file (ex: disk.raw)
-//   (string) partition - The partition number you are pulling the offset from
-// Output:
-//   (int) start - The start of the partition on the disk
-func getPartitionStart(partition, diskRaw string) (int, error) {
-	//create command
-	cmd1 := exec.Command("fdisk", "-l", diskRaw)
-	cmd2 := exec.Command("grep", diskRaw+partition)
-
-	reader, writer := io.Pipe()
-	var buf bytes.Buffer
-
-	cmd1.Stdout = writer
-	cmd2.Stdin = reader
-	cmd2.Stdout = &buf
-
-	cmd1.Start()
-	cmd2.Start()
-	cmd1.Wait()
-	writer.Close()
-	cmd2.Wait()
-	reader.Close()
-
-	words := strings.Fields(buf.String())
-	if len(words) < 2 {
-		return -1, errors.New("Error: " + diskRaw + " is not a valid DOS/MBR boot sector file")
-	}
-	start, err := strconv.Atoi(words[1])
-	if err != nil {
-		return -1, fmt.Errorf("failed to convert Ascii %v to string: %v", words[1], err)
-	}
-
-	return start, nil
-}
-
-// GetPartitionStructure returns the partition structure of .raw file
-// Input:
-//   (string) diskRaw - Path to the boot .raw file
-// Output:
-//   (string) partitionStructure - The output of the fdisk command
-func (image *ImageInfo) GetPartitionStructure() error {
-	if image.TempDir == "" {
-		return nil
-	}
-
-	out, err := exec.Command("sudo", "sgdisk", "-p", image.DiskFile).Output()
-	if err != nil {
-		return fmt.Errorf("failed to call sgdisk -p %v: %v", image.DiskFile, err)
-	}
-
-	partitionFile := filepath.Join(image.TempDir, "partitions.txt")
-	if err := utilities.WriteToNewFile(partitionFile, string(out[:])); err != nil {
-		return fmt.Errorf("failed create file %v and write %v: %v", partitionFile, string(out[:]), err)
-	}
-	image.PartitionFile = partitionFile
-	return nil
-}
-
-// mountDisk finds a free loop device and mounts a DOS/MBR disk file
-// Input:
-//   (string) diskFile - Name of DOS/MBR file (ex: disk.raw)
-//   (string) mountDir - Mount Destination
-//   (string) partition - The partition number you are pulling the offset from
-// Output:
-//   (string) loopDevice - Name of the loop device used to mount
-func mountDisk(diskFile, mountDir, partition string) (string, error) {
-	startOfPartition, err := getPartitionStart(partition, diskFile)
-	if err != nil {
-		return "", fmt.Errorf("failed to get start of partition #%v: %v", partition, err)
-	}
-	offset := strconv.Itoa(sectorSize * startOfPartition)
-
-	out, err := exec.Command("sudo", "losetup", "--show", "-fP", diskFile).Output()
-	if err != nil {
-		return "", fmt.Errorf("failed to create new loop device for %v: %v", diskFile, err)
-	}
-
-	loopDevice := string(out[:len(out)-1])
-	_, err = exec.Command("sudo", "mount", "-o", "ro,loop,offset="+offset, loopDevice, mountDir).Output()
-	if err != nil {
-		return "", fmt.Errorf("failed to mount loop device %v at %v: %v", loopDevice, mountDir, err)
-	}
-	return loopDevice, nil
-}
-
 // MountImage is an ImagInfo method that mounts partitions 1,3 and 12 of
 // the image into the temporary directory
 // Input:
@@ -175,7 +89,7 @@
 		}
 		image.StatePartition1 = stateful
 
-		loopDevice1, err := mountDisk(image.DiskFile, image.StatePartition1, "1")
+		loopDevice1, err := utilities.MountDisk(image.DiskFile, image.StatePartition1, "1")
 		if err != nil {
 			return fmt.Errorf("Failed to mount %v's partition #1 onto %v: %v", image.DiskFile, image.StatePartition1, err)
 		}
@@ -189,7 +103,7 @@
 		}
 		image.RootfsPartition3 = rootfs
 
-		loopDevice3, err := mountDisk(image.DiskFile, image.RootfsPartition3, "3")
+		loopDevice3, err := utilities.MountDisk(image.DiskFile, image.RootfsPartition3, "3")
 		if err != nil {
 			return fmt.Errorf("Failed to mount %v's partition #3 onto %v: %v", image.DiskFile, image.RootfsPartition3, err)
 		}
@@ -203,7 +117,7 @@
 		}
 		image.EFIPartition12 = efi
 
-		loopDevice12, err := mountDisk(image.DiskFile, image.EFIPartition12, "12")
+		loopDevice12, err := utilities.MountDisk(image.DiskFile, image.EFIPartition12, "12")
 		if err != nil {
 			return fmt.Errorf("Failed to mount %v's partition #12 onto %v: %v", image.DiskFile, image.EFIPartition12, err)
 		}
@@ -222,13 +136,20 @@
 	if gcsPath == "" {
 		return nil
 	}
-	gcsArray := strings.Split(gcsPath, "/")
-	if len(gcsArray) != 2 {
+	var gcsBucket, gcsObject string
+	if startOfBucket := strings.Index(gcsPath, "gs://"); startOfBucket < len(gcsPath)-5 {
+		gcsPath = gcsPath[startOfBucket+5:]
+	} else {
 		printUsage()
-		return errors.New("Error: Argument " + gcsPath + " is not a valid gcs path (\"/\" separators)")
+		return errors.New("Error: Argument " + gcsPath + " is not a valid gcs path \"gs://<bucket>/<object_path>.tar.gz\"")
 	}
-	gcsBucket := gcsArray[0]
-	gcsObject := gcsArray[1]
+	if startOfObject := strings.Index(gcsPath, "/"); startOfObject > 0 && startOfObject < len(gcsPath)-1 {
+		gcsBucket = gcsPath[:startOfObject]
+		gcsObject = gcsPath[startOfObject+1:]
+	} else {
+		printUsage()
+		return errors.New("Error: Argument " + gcsPath + " is not a valid gcs path \"gs://<bucket>/<object_path>.tar.gz\"")
+	}
 
 	tempDir, err := ioutil.TempDir(".", "tempDir") // Removed at end
 	if err != nil {
@@ -236,7 +157,7 @@
 	}
 	image.TempDir = tempDir
 
-	tarFile, err := utilities.GcsDowndload(gcsBucket, gcsObject, image.TempDir)
+	tarFile, err := utilities.GcsDowndload(gcsBucket, gcsObject, image.TempDir, filepath.Base(gcsObject))
 	if err != nil {
 		return fmt.Errorf("failed to download GCS object %v from bucket %v: %v", gcsObject, gcsBucket, err)
 	}
@@ -249,35 +170,6 @@
 	return nil
 }
 
-// ValidateLocalImages ensures the two images are one or two unique boot files
-// Input:
-//   (string) localPath1 - Local path to the first disk.raw file
-//   (string) localPath2 - Local path to the second disk.raw file
-// Output: nil on success, else error
-func ValidateLocalImages(localPath1, localPath2 string) error {
-	if localPath2 == "" {
-		if res := utilities.FileExists(localPath1, "raw"); res == -1 {
-			return errors.New("Error: " + localPath1 + " file does not exist")
-		} else if res == 0 {
-			return errors.New("Error: " + localPath1 + " is not a \".raw\" file")
-		}
-		return nil
-	}
-
-	if res := utilities.FileExists(localPath2, "raw"); res == -1 {
-		return errors.New("Error: " + localPath2 + " file does not exist")
-	} else if res == 0 {
-		return errors.New("Error: " + localPath2 + " is not a \".raw\" file")
-	}
-
-	info1, _ := os.Stat(localPath1)
-	info2, _ := os.Stat(localPath2)
-	if os.SameFile(info1, info2) {
-		return errors.New("Error: Identical image passed in. To analyze single image, pass in one argument")
-	}
-	return nil
-}
-
 // GetLocalImage is an ImageInfo method that creates a temporary directory
 // to loop device mount the disk.raw file stored on the local file system
 // Input:
@@ -376,7 +268,7 @@
 		return fmt.Errorf("failed to export %v cos image to GCS bucket %v: %v", publicCosImage, gcsBucket, err)
 	}
 
-	gcsPath := filepath.Join(gcsBucket, publicCosImage+gcsObjFormat)
+	gcsPath := filepath.Join(gcsBucket, publicCosImage, gcsObjFormat)
 	if err := image.GetGcsImage(gcsPath); err != nil {
 		return fmt.Errorf("failed to download image stored on GCS for %v: %v", gcsPath, err)
 	}
diff --git a/src/cmd/cos_image_analyzer/internal/input/parse.go b/src/cmd/cos_image_analyzer/internal/input/parse.go
index 344f648..b489622 100644
--- a/src/cmd/cos_image_analyzer/internal/input/parse.go
+++ b/src/cmd/cos_image_analyzer/internal/input/parse.go
@@ -4,6 +4,7 @@
 	"errors"
 	"flag"
 	"fmt"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
@@ -11,7 +12,14 @@
 	"cos.googlesource.com/cos/tools/src/cmd/cos_image_analyzer/internal/utilities"
 )
 
-var binaryDiffTypes = []string{"Version", "BuildID", "Rootfs", "Kernel-command-line", "Stateful-partition", "Partition-structure", "Sysctl-settings", "OS-config"}
+// BinaryDiffTypes is a list of all valid binary differnce types
+var BinaryDiffTypes = []string{"Version", "BuildID", "Rootfs", "Kernel-command-line", "Stateful-partition", "Partition-structure", "Sysctl-settings", "OS-config", "Kernel-configs"}
+
+// Default Rootfs entires that are overridden by the "compress-rootfs" flag
+var defaultCompressRootfs = []string{"/bin/", "/lib/modules/", "/lib64/", "/usr/libexec/", "/usr/bin/", "/usr/sbin/", "/usr/lib64/", "/usr/share/zoneinfo/", "/usr/share/git/", "/usr/lib/", "/sbin/", "/etc/ssh/", "/etc/os-release/", "/etc/package_list/"}
+
+// Default Stateful entires that are overridden by the "compress-stateful" flag
+var defaultCompressStateful = []string{"/var_overlay/db/"}
 
 // Custom usage function. See -h flag
 func printUsage() {
@@ -22,13 +30,13 @@
 SYNOPSIS
 	%s [-local] FILE-1 [FILE-2] (default true)
 		FILE - the local file path to the DOS/MBR boot sector file of your image (Ex: disk.raw)
-		Ex: %s -local image-cos-77-12371-273-0/disk.raw image-cos-81-12871-119-0/disk.raw
+		Ex: %s image-cos-77-12371-273-0/disk.raw image-cos-81-12871-119-0/disk.raw
 
-	%s -local -binary=Sysctl-settings,OS-config -release-notes=false cos-77-12371/disk.raw
+	%s -local -binary=Sysctl-settings,OS-config -package=false image-cos-77-12371-273-0/disk.raw
 
 	%s -gcs GCS-PATH-1 [GCS-PATH-2]
 		GCS-PATH - the GCS "bucket/object" path for the COS Image ("object" is type .tar.gz)
-		Ex: %s -gcs my-bucket/cos-77-12371-273-0.tar.gz my-bucket/cos-81-12871-119-0.tar.gz
+		Ex: %s -gcs my-bucket/cos-images/cos-77-12371-273-0.tar.gz my-bucket/cos-images/cos-81-12871-119-0.tar.gz
 
 
 DESCRIPTION
@@ -41,18 +49,13 @@
 
 	Difference Flags:
 	-binary (string)
-		specify which type of binary difference to show. Types "Version", "BuildID", "Rootfs", "Kernel-command-line",
-		"Stateful-partition", "Partition-structure", "Sysctl-settings", and "OS-config" are supported. To list
-		multiple types separate by comma. To NOT list any binary difference, set flag to "false". (default all types)
+		specify which type of binary difference to show. Types "Version", "BuildID", "Kernel-command-line",
+		"Partition-structure", "Sysctl-settings", and "Kernel-configs" are supported for one and two image. "Rootfs",
+		"Stateful-partition", and "OS-config" are only supported for two images. To list multiple types separate by
+		comma. To NOT list any binary difference, set flag to "false". (default all types)
 	-package
 		specify whether to show package difference. Shows addition/removal of packages and package version updates.
 		To NOT list any package difference, set flag to false. (default true)
-	-commit
-		specify whether to show commit difference. Shows commit changelog between the two images.
-		To NOT list any commit difference, set flag to false. (default true)
-	-release-notes
-		specify whether to show release notes difference. Shows differences in human-written release notes between
-		the two images. To NOT list any release notes difference, set flag to false. (default true)
 
 	Attribute Flags
 	-verbose
@@ -60,14 +63,14 @@
 		"internal/binary/CompressRootfs.txt", "internal/binary/CompressStateful.txt", and "internal/binary/CompressOSConfigs.txt"
 		files to edit the default directories whose differences are compressed together.
 	-compress-rootfs (string)
-		to customize which directories are compressed in a non-verbose Rootfs and OS-config difference output, provide a local 
+		to customize which directories are compressed in a non-verbose Rootfs and OS-config difference output, provide a local
 		file path to a text file. Format of the file must be one root file path per line with an ending back slash and no commas.
 		By default the directory(s) that are compressed during a diff are /bin/, /lib/modules/, /lib64/, /usr/libexec/, /usr/bin/,
 		/usr/sbin/, /usr/lib64/, /usr/share/zoneinfo/, /usr/share/git/, /usr/lib/, and /sbin/.
 	-compress-stateful (string)
 		to customize which directories are compressed in a non-verbose Stateful-partition difference output, provide a local
 		file path to a text file. Format of file must be one root file path per line with no commas. By default the directory(s)
-		that are compressed during a diff are /var_overlay/db/. 
+		that are compressed during a diff are /var_overlay/db/.
 
 	Output Flags:
 	-output (string)
@@ -96,28 +99,30 @@
 	}
 
 	if flagInfo.BinaryDiffPtr == "" {
-		flagInfo.BinaryTypesSelected = binaryDiffTypes
+		flagInfo.BinaryTypesSelected = BinaryDiffTypes
 	} else {
 		binaryTypesSelected := strings.Split(flagInfo.BinaryDiffPtr, ",")
 		for _, elem := range binaryTypesSelected {
-			if utilities.InArray(elem, binaryDiffTypes) {
+			if utilities.InArray(elem, BinaryDiffTypes) {
 				flagInfo.BinaryTypesSelected = append(flagInfo.BinaryTypesSelected, elem)
-			} else {
+			} else if elem != "false" {
 				return errors.New("Error: Invalid option for \"-binary\" flag")
 			}
 		}
 	}
-
-	if res := utilities.FileExists(flagInfo.CompressRootfsFile, "txt"); res == -1 {
-		return errors.New("Error: " + flagInfo.CompressRootfsFile + " file does not exist")
-	} else if res == 0 {
-		return errors.New("Error: " + flagInfo.CompressRootfsFile + " is not a \".txt\" file")
+	if flagInfo.CompressRootfsFile != "" {
+		if res := utilities.FileExists(flagInfo.CompressRootfsFile, "txt"); res == -1 {
+			return errors.New("Error: " + flagInfo.CompressRootfsFile + " file does not exist")
+		} else if res == 0 {
+			return errors.New("Error: " + flagInfo.CompressRootfsFile + " is not a \".txt\" file")
+		}
 	}
-
-	if res := utilities.FileExists(flagInfo.CompressStatefulFile, "txt"); res == -1 {
-		return errors.New("Error: " + flagInfo.CompressStatefulFile + " file does not exist")
-	} else if res == 0 {
-		return errors.New("Error: " + flagInfo.CompressStatefulFile + " is not a \".txt\" file")
+	if flagInfo.CompressStatefulFile != "" {
+		if res := utilities.FileExists(flagInfo.CompressStatefulFile, "txt"); res == -1 {
+			return errors.New("Error: " + flagInfo.CompressStatefulFile + " file does not exist")
+		} else if res == 0 {
+			return errors.New("Error: " + flagInfo.CompressStatefulFile + " is not a \".txt\" file")
+		}
 	}
 
 	if flagInfo.OutputSelected != "terminal" && flagInfo.OutputSelected != "json" {
@@ -159,8 +164,8 @@
 	flag.BoolVar(&flagInfo.ReleaseNotesSelected, "release-notes", true, "")
 
 	flag.BoolVar(&flagInfo.Verbose, "verbose", false, "")
-	flag.StringVar(&flagInfo.CompressRootfsFile, "compress-rootfs", "internal/binary/CompressRootfs.txt", "")
-	flag.StringVar(&flagInfo.CompressStatefulFile, "compress-stateful", "internal/binary/CompressStateful.txt", "")
+	flag.StringVar(&flagInfo.CompressRootfsFile, "compress-rootfs", "", "")
+	flag.StringVar(&flagInfo.CompressStatefulFile, "compress-stateful", "", "")
 
 	flag.StringVar(&flagInfo.OutputSelected, "output", "terminal", "")
 	flag.Parse()
@@ -169,9 +174,58 @@
 		printUsage()
 		return &FlagInfo{}, err
 	}
+
+	if flagInfo.CompressRootfsFile != "" { // Get CompressRootfsslice
+		compressRootsBytes, err := ioutil.ReadFile(flagInfo.CompressRootfsFile)
+		if err != nil {
+			return &FlagInfo{}, fmt.Errorf("failed to read compress-rootfs file %v: %v", flagInfo.CompressRootfsFile, err)
+		}
+		flagInfo.CompressRootfsSlice = strings.Split(string(compressRootsBytes), "\n")
+	} else {
+		flagInfo.CompressRootfsSlice = defaultCompressRootfs
+	}
+
+	if flagInfo.CompressStatefulFile != "" { // Get CompressStatefulFileSlice
+		compressedStatefulBytes, err := ioutil.ReadFile(flagInfo.CompressStatefulFile)
+		if err != nil {
+			return &FlagInfo{}, fmt.Errorf("failed to read compress-stateful file %v: %v", flagInfo.CompressStatefulFile, err)
+		}
+		flagInfo.CompressStatefulSlice = strings.Split(string(compressedStatefulBytes), "\n")
+	} else {
+		flagInfo.CompressStatefulSlice = defaultCompressStateful
+	}
 	return flagInfo, nil
 }
 
+// validateLocalImages ensures the two images are one or two unique boot files
+// Input:
+//   (string) localPath1 - Local path to the first disk.raw file
+//   (string) localPath2 - Local path to the second disk.raw file
+// Output: nil on success, else error
+func validateLocalImages(localPath1, localPath2 string) error {
+	if localPath2 == "" {
+		if res := utilities.FileExists(localPath1, "raw"); res == -1 {
+			return errors.New("Error: " + localPath1 + " file does not exist")
+		} else if res == 0 {
+			return errors.New("Error: " + localPath1 + " is not a \".raw\" file")
+		}
+		return nil
+	}
+
+	if res := utilities.FileExists(localPath2, "raw"); res == -1 {
+		return errors.New("Error: " + localPath2 + " file does not exist")
+	} else if res == 0 {
+		return errors.New("Error: " + localPath2 + " is not a \".raw\" file")
+	}
+
+	info1, _ := os.Stat(localPath1)
+	info2, _ := os.Stat(localPath2)
+	if os.SameFile(info1, info2) {
+		return errors.New("Error: Identical image passed in. To analyze single image, pass in one argument")
+	}
+	return nil
+}
+
 // GetImages reads in all the flags and handles the input based on its type.
 // Input:
 //   (*FlagInfo) flagInfo - A struct that holds input preference from the user
@@ -208,7 +262,7 @@
 	} else if flagInfo.LocalPtr {
 		localPath1, localPath2 := flagInfo.Image1, flagInfo.Image2
 
-		if err := ValidateLocalImages(localPath1, localPath2); err != nil {
+		if err := validateLocalImages(localPath1, localPath2); err != nil {
 			return image1, image2, fmt.Errorf("failed to validate local images: %v", err)
 		}
 		if err := image1.GetLocalImage(localPath1); err != nil {
diff --git a/src/cmd/cos_image_analyzer/internal/output/imagediff.go b/src/cmd/cos_image_analyzer/internal/output/imagediff.go
index 423f91f..59c6a48 100644
--- a/src/cmd/cos_image_analyzer/internal/output/imagediff.go
+++ b/src/cmd/cos_image_analyzer/internal/output/imagediff.go
@@ -17,11 +17,13 @@
 // Formater is a ImageDiff function that outputs the image differences based on the "-output" flag.
 // Either to the terminal (default) or to a stored json object
 // Input:
+//   (string) image1 - Temp directory name of image1
+//   (string) image2 - Temp directory name of image2
 //   (*FlagInfo) flagInfo - A struct that holds input preference from the user
 // Output:
 //   ([]string) diffstrings/jsonObjectStr - Based on "-output" flag, either formated string
 //   for the terminal or a string json object
-func (imageDiff *ImageDiff) Formater(flagInfo *input.FlagInfo) (string, error) {
+func (imageDiff *ImageDiff) Formater(image1, image2 string, flagInfo *input.FlagInfo) (string, error) {
 	if flagInfo.OutputSelected == "terminal" {
 		binaryStrings := ""
 		binaryFunctions := map[string]func() string{
@@ -31,8 +33,11 @@
 			"Stateful-partition":  imageDiff.BinaryDiff.FormatStatefulDiff,
 			"OS-config":           imageDiff.BinaryDiff.FormatOSConfigDiff,
 			"Partition-structure": imageDiff.BinaryDiff.FormatPartitionStructureDiff,
+			"Kernel-configs":      imageDiff.BinaryDiff.FormatKernelConfigsDiff,
+			"Kernel-command-line": imageDiff.BinaryDiff.FormatKernelCommandLineDiff,
+			"Sysctl-settings":     imageDiff.BinaryDiff.FormatSysctlSettingsDiff,
 		}
-		for diff := range binaryFunctions {
+		for _, diff := range input.BinaryDiffTypes {
 			if utilities.InArray(diff, flagInfo.BinaryTypesSelected) {
 				binaryStrings += binaryFunctions[diff]()
 			}
@@ -40,9 +45,9 @@
 
 		if len(binaryStrings) > 0 {
 			if flagInfo.Image2 == "" {
-				binaryStrings = "================= Binary Info =================\n" + binaryStrings
+				binaryStrings = "================= Binary Info =================\nImage: " + image1 + "\n" + binaryStrings
 			} else {
-				binaryStrings = "================= Binary Differences =================\n" + binaryStrings
+				binaryStrings = "================= Binary Differences =================\nImages: " + image1 + " and " + image2 + "\n" + binaryStrings
 			}
 		}
 
diff --git a/src/cmd/cos_image_analyzer/internal/testdata/CompressRootfsFile.txt b/src/cmd/cos_image_analyzer/internal/testdata/CompressRootfsFile.txt
deleted file mode 100644
index 6219d3d..0000000
--- a/src/cmd/cos_image_analyzer/internal/testdata/CompressRootfsFile.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-/proc/
-/usr/lib/
-/docker/util/
\ No newline at end of file
diff --git a/src/cmd/cos_image_analyzer/internal/testdata/CompressStatefulFile.txt b/src/cmd/cos_image_analyzer/internal/testdata/CompressStatefulFile.txt
deleted file mode 100644
index 2e3c219..0000000
--- a/src/cmd/cos_image_analyzer/internal/testdata/CompressStatefulFile.txt
+++ /dev/null
@@ -1 +0,0 @@
-/var_overlay/
\ No newline at end of file
diff --git a/src/cmd/cos_image_analyzer/internal/testdata/image1/efi/efi/boot/grub.cfg b/src/cmd/cos_image_analyzer/internal/testdata/image1/efi/efi/boot/grub.cfg
new file mode 100644
index 0000000..6fba5a3
--- /dev/null
+++ b/src/cmd/cos_image_analyzer/internal/testdata/image1/efi/efi/boot/grub.cfg
@@ -0,0 +1,35 @@
+defaultA=2
+defaultB=3
+gptpriority $grubdisk 2 prioA
+gptpriority $grubdisk 4 prioB
+
+if [ $prioA -lt $prioB ]; then
+  set default=$defaultB
+else
+  set default=$defaultA
+fi
+
+set timeout=0
+
+# NOTE: These magic grub variables are a Chrome OS hack. They are not portable.
+
+menuentry "local image A" {
+  linux /syslinux/vmlinuz.A init=/usr/lib/systemd/systemd boot=local rootwait ro noresume noswap loglevel=7 noinitrd console=ttyS0 security=apparmor virtio_net.napi_tx=1 systemd.unified_cgroup_hierarchy=false systemd.legacy_systemd_cgroup_controller=false csm.disabled=1  i915.modeset=1 cros_efi       root=PARTUUID=E5822204-E5B9-2848-8A90-37790091EA3E
+}
+
+menuentry "local image B" {
+  linux /syslinux/vmlinuz.B init=/usr/lib/systemd/systemd boot=local rootwait ro noresume noswap loglevel=7 noinitrd console=ttyS0 security=apparmor virtio_net.napi_tx=1 systemd.unified_cgroup_hierarchy=false systemd.legacy_systemd_cgroup_controller=false csm.disabled=1  i915.modeset=1 cros_efi       root=PARTUUID=BA6C8ED3-DD99-F046-BF74-3E2CF0F06234
+}
+
+menuentry "verified image A" {
+  linux /syslinux/vmlinuz.A init=/usr/lib/systemd/systemd boot=local rootwait ro noresume noswap loglevel=7 noinitrd console=ttyS0 security=apparmor virtio_net.napi_tx=1 systemd.unified_cgroup_hierarchy=false systemd.legacy_systemd_cgroup_controller=false csm.disabled=1  dm_verity.error_behavior=3 dm_verity.max_bios=-1 dm_verity.dev_wait=1       i915.modeset=1 cros_efi root=/dev/dm-0 dm="1 vroot none ro 1,0 2539520 verity payload=PARTUUID=E5822204-E5B9-2848-8A90-37790091EA3E hashtree=PARTUUID=E5822204-E5B9-2848-8A90-37790091EA3E hashstart=2539520 alg=sha256 root_hexdigest=f24f966c2c8e2dab5caeffd2ca4c406f31d3a7f4ffb3bcf578bd96c535bc01be salt=096198c60913c02972a43985fc8bb97ceb5359e75389d7ce7e3238139dd2078b"
+}
+
+menuentry "verified image B" {
+  linux /syslinux/vmlinuz.B init=/usr/lib/systemd/systemd boot=local rootwait ro noresume noswap loglevel=7 noinitrd console=ttyS0 security=apparmor virtio_net.napi_tx=1 systemd.unified_cgroup_hierarchy=false systemd.legacy_systemd_cgroup_controller=false csm.disabled=1  dm_verity.error_behavior=3 dm_verity.max_bios=-1 dm_verity.dev_wait=1       i915.modeset=1 cros_efi root=/dev/dm-0 dm="1 vroot none ro 1,0 2539520 verity payload=PARTUUID=BA6C8ED3-DD99-F046-BF74-3E2CF0F06234 hashtree=PARTUUID=BA6C8ED3-DD99-F046-BF74-3E2CF0F06234 hashstart=2539520 alg=sha256 root_hexdigest=f24f966c2c8e2dab5caeffd2ca4c406f31d3a7f4ffb3bcf578bd96c535bc01be salt=096198c60913c02972a43985fc8bb97ceb5359e75389d7ce7e3238139dd2078b"
+}
+
+# FIXME: usb doesn't support verified boot for now
+menuentry "Alternate USB Boot" {
+  linux (hd0,3)/boot/vmlinuz init=/usr/lib/systemd/systemd boot=local rootwait ro noresume noswap loglevel=7 noinitrd console=ttyS0 security=apparmor virtio_net.napi_tx=1 systemd.unified_cgroup_hierarchy=false systemd.legacy_systemd_cgroup_controller=false csm.disabled=1  root=PARTUUID=E5822204-E5B9-2848-8A90-37790091EA3E i915.modeset=1 cros_efi
+}
diff --git a/src/cmd/cos_image_analyzer/internal/testdata/image1/partitions.txt b/src/cmd/cos_image_analyzer/internal/testdata/image1/partitions.txt
index 928e759..dbc708e 100644
--- a/src/cmd/cos_image_analyzer/internal/testdata/image1/partitions.txt
+++ b/src/cmd/cos_image_analyzer/internal/testdata/image1/partitions.txt
@@ -19,4 +19,4 @@
    9           16450           16450   512 bytes   7F02  reserved
   10           16451           16451   512 bytes   7F02  reserved
   11              64           16447   8.0 MiB     EF02  RWFW
-  12          249856          315391   32.0 MiB    EF00  EFI-SYSTEM
+  12          249856          315391   32.0 MiB    EF00  EFI-SYSTEM
\ No newline at end of file
diff --git a/src/cmd/cos_image_analyzer/internal/testdata/image1/rootfs/etc/sysctl.d/00-sysctl.conf b/src/cmd/cos_image_analyzer/internal/testdata/image1/rootfs/etc/sysctl.d/00-sysctl.conf
new file mode 100644
index 0000000..27dfc84
--- /dev/null
+++ b/src/cmd/cos_image_analyzer/internal/testdata/image1/rootfs/etc/sysctl.d/00-sysctl.conf
@@ -0,0 +1,11 @@
+# /etc/sysctl.conf
+# Look in /proc/sys/ for all the things you can setup.
+#
+
+# Enables source route verification
+net.ipv4.conf.default.rp_filter = 1
+# Enable reverse path
+net.ipv4.conf.all.rp_filter = 1
+
+# Disable shrinking the cwnd when connection is idle
+net.ipv4.tcp_slow_start_after_idle = 0
\ No newline at end of file
diff --git a/src/cmd/cos_image_analyzer/internal/testdata/image1/usr/src/linux-headers-4.19.112+/.config b/src/cmd/cos_image_analyzer/internal/testdata/image1/usr/src/linux-headers-4.19.112+/.config
new file mode 100644
index 0000000..bd58ab6
--- /dev/null
+++ b/src/cmd/cos_image_analyzer/internal/testdata/image1/usr/src/linux-headers-4.19.112+/.config
@@ -0,0 +1,14 @@
+#
+# Compiler: Chromium OS 10.0_pre377782_p20200113-r10 clang version 10.0.0 (/var/cache/chromeos-cache/distfiles/host/egit-src/llvm-project 4e8231b5cf0f5f62c7a51a857e29f5be5cb55734)
+#
+CONFIG_GCC_VERSION=0
+CONFIG_CC_IS_CLANG=y
+CONFIG_CLANG_VERSION=100000
+
+#
+# General setup
+#
+CONFIG_INIT_ENV_ARG_LIMIT=32
+CONFIG_LOCALVERSION=""
+CONFIG_BUILD_SALT=""
+CONFIG_HAVE_KERNEL_GZIP=y
\ No newline at end of file
diff --git a/src/cmd/cos_image_analyzer/internal/testdata/image2/efi/efi/boot/grub.cfg b/src/cmd/cos_image_analyzer/internal/testdata/image2/efi/efi/boot/grub.cfg
new file mode 100755
index 0000000..58a9425
--- /dev/null
+++ b/src/cmd/cos_image_analyzer/internal/testdata/image2/efi/efi/boot/grub.cfg
@@ -0,0 +1,35 @@
+defaultA=2
+defaultB=3
+gptpriority $grubdisk 2 prioA
+gptpriority $grubdisk 4 prioB
+
+if [ $prioA -lt $prioB ]; then
+  set default=$defaultB
+else
+  set default=$defaultA
+fi
+
+set timeout=0
+
+# NOTE: These magic grub variables are a Chrome OS hack. They are not portable.
+
+menuentry "local image A" {
+  linux /syslinux/vmlinuz.A init=/usr/lib/systemd/systemd boot=local rootwait ro noresume noswap loglevel=7 noinitrd console=ttyS0 security=apparmor virtio_net.napi_tx=1 systemd.unified_cgroup_hierarchy=false systemd.legacy_systemd_cgroup_controller=false csm.disabled=1  i915.modeset=1 cros_efi       root=PARTUUID=D8242B83-5E09-2247-9220-A5581F2ADD0B
+}
+
+menuentry "local image B" {
+  linux /syslinux/vmlinuz.B init=/usr/lib/systemd/systemd boot=local rootwait ro noresume noswap loglevel=7 noinitrd console=ttyS0 security=apparmor virtio_net.napi_tx=1 systemd.unified_cgroup_hierarchy=false systemd.legacy_systemd_cgroup_controller=false csm.disabled=1  i915.modeset=1 cros_efi       root=PARTUUID=D09026B0-6BC4-3C4A-A0D5-F7B8C67E96CC
+}
+
+menuentry "verified image A" {
+  linux /syslinux/vmlinuz.A init=/usr/lib/systemd/systemd boot=local rootwait ro noresume noswap loglevel=7 noinitrd console=ttyS0 security=apparmor virtio_net.napi_tx=1 systemd.unified_cgroup_hierarchy=false systemd.legacy_systemd_cgroup_controller=false csm.disabled=1  dm_verity.error_behavior=3 dm_verity.max_bios=-1 dm_verity.dev_wait=1       i915.modeset=1 cros_efi root=/dev/dm-0 dm="1 vroot none ro 1,0 4077568 verity payload=PARTUUID=D8242B83-5E09-2247-9220-A5581F2ADD0B hashtree=PARTUUID=D8242B83-5E09-2247-9220-A5581F2ADD0B hashstart=4077568 alg=sha256 root_hexdigest=1fcc4da1d3e2fa974479ac43cd3fdcc8127ede293888bfb7b0838fa795b4faeb salt=4b4c38200531b5c2f8bc29d1b7fe9f123da0c493ac086cc2dd5084888a61ce4b"
+}
+
+menuentry "verified image B" {
+  linux /syslinux/vmlinuz.B init=/usr/lib/systemd/systemd boot=local rootwait ro noresume noswap loglevel=7 noinitrd console=ttyS0 security=apparmor virtio_net.napi_tx=1 systemd.unified_cgroup_hierarchy=false systemd.legacy_systemd_cgroup_controller=false csm.disabled=1  dm_verity.error_behavior=3 dm_verity.max_bios=-1 dm_verity.dev_wait=1       i915.modeset=1 cros_efi root=/dev/dm-0 dm="1 vroot none ro 1,0 4077568 verity payload=PARTUUID=D09026B0-6BC4-3C4A-A0D5-F7B8C67E96CC hashtree=PARTUUID=D09026B0-6BC4-3C4A-A0D5-F7B8C67E96CC hashstart=4077568 alg=sha256 root_hexdigest=1fcc4da1d3e2fa974479ac43cd3fdcc8127ede293888bfb7b0838fa795b4faeb salt=4b4c38200531b5c2f8bc29d1b7fe9f123da0c493ac086cc2dd5084888a61ce4b"
+}
+
+# FIXME: usb doesn't support verified boot for now
+menuentry "Alternate USB Boot" {
+  linux (hd0,3)/boot/vmlinuz init=/usr/lib/systemd/systemd boot=local rootwait ro noresume noswap loglevel=7 noinitrd console=ttyS0 security=apparmor virtio_net.napi_tx=1 systemd.unified_cgroup_hierarchy=false systemd.legacy_systemd_cgroup_controller=false csm.disabled=1  root=PARTUUID=D8242B83-5E09-2247-9220-A5581F2ADD0B i915.modeset=1 cros_efi
+}
diff --git a/src/cmd/cos_image_analyzer/internal/testdata/image2/partitions.txt b/src/cmd/cos_image_analyzer/internal/testdata/image2/partitions.txt
index d202b1a..ad5e7b3 100644
--- a/src/cmd/cos_image_analyzer/internal/testdata/image2/partitions.txt
+++ b/src/cmd/cos_image_analyzer/internal/testdata/image2/partitions.txt
@@ -19,4 +19,4 @@
    9           16450           16450   512 bytes   7F02  reserved
   10           16451           16451   512 bytes   7F02  reserved
   11              64           16447   8.0 MiB     EF02  RWFW
-  12          249856          315391   32.0 MiB    EF00  EFI-SYSTEM
+  12          249856          315391   32.0 MiB    EF00  EFI-SYSTEM
\ No newline at end of file
diff --git a/src/cmd/cos_image_analyzer/internal/testdata/image2/rootfs/etc/sysctl.d/00-sysctl.conf b/src/cmd/cos_image_analyzer/internal/testdata/image2/rootfs/etc/sysctl.d/00-sysctl.conf
new file mode 100644
index 0000000..2484c00
--- /dev/null
+++ b/src/cmd/cos_image_analyzer/internal/testdata/image2/rootfs/etc/sysctl.d/00-sysctl.conf
@@ -0,0 +1,14 @@
+# /etc/sysctl.conf
+# Look in /proc/sys/ for all the things you can setup.
+#
+
+# Enables source route verification
+net.ipv4.conf.default.rp_filter = 1
+# Enable reverse path
+net.ipv4.conf.all.rp_filter = 2
+
+# Disable shrinking the cwnd when connection is idle
+net.ipv4.tcp_slow_start_after_idle = 1
+
+# dumby variable
+net.ipv4.conf = 2
\ No newline at end of file
diff --git a/src/cmd/cos_image_analyzer/internal/testdata/image2/usr/src/linux-headers-4.19.112+/.config b/src/cmd/cos_image_analyzer/internal/testdata/image2/usr/src/linux-headers-4.19.112+/.config
new file mode 100644
index 0000000..75cd72a
--- /dev/null
+++ b/src/cmd/cos_image_analyzer/internal/testdata/image2/usr/src/linux-headers-4.19.112+/.config
@@ -0,0 +1,16 @@
+#
+# Compiler: Chromium OS 9.0_pre361749_p20190714-r4 clang version 9.0.0 (/var/cache/chromeos-cache/distfiles/host/egit-src/llvm-project c11de5eada2decd0a495ea02676b6f4838cd54fb) (based on LLVM 9.0.0svn)
+#
+CONFIG_GCC_VERSION=0
+CONFIG_CC_IS_CLANG=y
+CONFIG_CLANG_VERSION=90000
+
+#
+# General setup
+#
+CONFIG_INIT_ENV_ARG_LIMIT=32
+# CONFIG_COMPILE_TEST is not set
+CONFIG_LOCALVERSION=""
+# CONFIG_LOCALVERSION_AUTO is not set
+CONFIG_BUILD_SALT=""
+CONFIG_HAVE_KERNEL_GZIP=y
diff --git a/src/cmd/cos_image_analyzer/internal/utilities/gcs_download.go b/src/cmd/cos_image_analyzer/internal/utilities/gcs_download.go
index 6403a2f..8b1a8c1 100644
--- a/src/cmd/cos_image_analyzer/internal/utilities/gcs_download.go
+++ b/src/cmd/cos_image_analyzer/internal/utilities/gcs_download.go
@@ -22,9 +22,10 @@
 //   (string) bucket - Name of the GCS bucket
 //   (string) object - Name of the GCS object
 //   (string) destDir - Destination for downloaded GCS object
+//   (string) name - Name for the downloaded file
 // Output:
 //   (string) downloadedFile - Path to downloaded GCS object
-func GcsDowndload(bucket, object, destDir string) (string, error) {
+func GcsDowndload(bucket, object, destDir, name string) (string, error) {
 	// Call API to download GCS object into tempDir
 	ctx := context.Background()
 	client, err := storage.NewClient(ctx)
@@ -42,7 +43,7 @@
 	}
 	defer rc.Close()
 
-	downloadedFile, err := os.Create(filepath.Join(destDir, object))
+	downloadedFile, err := os.Create(filepath.Join(destDir, name))
 	if err != nil {
 		return "", fmt.Errorf("failed to create file %v/%v: %v", destDir, object, err)
 	}
@@ -54,6 +55,6 @@
 	}
 	bytesStr := strconv.FormatInt(bytesDownloaded, base10)
 
-	log.Print("Blob " + object + " downloaded. \n" + bytesStr + " Total Bytes")
+	log.Print("GCS object: ", object, " downloaded from GCS bucket: ", bucket, ". Total bytes ", bytesStr)
 	return downloadedFile.Name(), nil
 }
diff --git a/src/cmd/cos_image_analyzer/internal/utilities/logic_helper.go b/src/cmd/cos_image_analyzer/internal/utilities/logic_helper.go
index 04b605d..a55efc9 100644
--- a/src/cmd/cos_image_analyzer/internal/utilities/logic_helper.go
+++ b/src/cmd/cos_image_analyzer/internal/utilities/logic_helper.go
@@ -1,13 +1,18 @@
 package utilities
 
 import (
+	"bytes"
+	"errors"
 	"fmt"
 	"io"
 	"os"
 	"os/exec"
+	"strconv"
 	"strings"
 )
 
+const sectorSize = 512
+
 // InArray determines if a string appears in a string array
 func InArray(val string, arr []string) bool {
 	for _, elem := range arr {
@@ -75,9 +80,73 @@
 	return output
 }
 
+// getPartitionStart finds the start partition offset of the disk
+// Input:
+//   (string) diskFile - Name of DOS/MBR file (ex: disk.raw)
+//   (string) partition - The partition number you are pulling the offset from
+// Output:
+//   (int) start - The start of the partition on the disk
+func getPartitionStart(partition, diskRaw string) (int, error) {
+	//create command
+	cmd1 := exec.Command("fdisk", "-l", diskRaw)
+	cmd2 := exec.Command("grep", diskRaw+partition)
+
+	reader, writer := io.Pipe()
+	var buf bytes.Buffer
+
+	cmd1.Stdout = writer
+	cmd2.Stdin = reader
+	cmd2.Stdout = &buf
+
+	cmd1.Start()
+	cmd2.Start()
+	cmd1.Wait()
+	writer.Close()
+	cmd2.Wait()
+	reader.Close()
+
+	words := strings.Fields(buf.String())
+	if len(words) < 2 {
+		return -1, errors.New("Error: " + diskRaw + " is not a valid DOS/MBR boot sector file")
+	}
+	start, err := strconv.Atoi(words[1])
+	if err != nil {
+		return -1, fmt.Errorf("failed to convert Ascii %v to string: %v", words[1], err)
+	}
+
+	return start, nil
+}
+
+// MountDisk finds a free loop device and mounts a DOS/MBR disk file
+// Input:
+//   (string) diskFile - Name of DOS/MBR file (ex: disk.raw)
+//   (string) mountDir - Mount Destination
+//   (string) partition - The partition number you are pulling the offset from
+// Output:
+//   (string) loopDevice - Name of the loop device used to mount
+func MountDisk(diskFile, mountDir, partition string) (string, error) {
+	startOfPartition, err := getPartitionStart(partition, diskFile)
+	if err != nil {
+		return "", fmt.Errorf("failed to get start of partition #%v: %v", partition, err)
+	}
+	offset := strconv.Itoa(sectorSize * startOfPartition)
+
+	out, err := exec.Command("sudo", "losetup", "--show", "-fP", diskFile).Output()
+	if err != nil {
+		return "", fmt.Errorf("failed to create new loop device for %v: %v", diskFile, err)
+	}
+
+	loopDevice := string(out[:len(out)-1])
+	_, err = exec.Command("sudo", "mount", "-o", "ro,loop,offset="+offset, loopDevice, mountDir).Output()
+	if err != nil {
+		return "", fmt.Errorf("failed to mount loop device %v at %v: %v", loopDevice, mountDir, err)
+	}
+	return loopDevice, nil
+}
+
 // Unmount umounts a mounted directory and deletes its loop device
 func Unmount(mountedDirectory, loopDevice string) error {
-	if _, err := exec.Command("sudo", "umount", mountedDirectory).Output(); err != nil {
+	if _, err := exec.Command("sudo", "umount", "-l", mountedDirectory).Output(); err != nil {
 		return fmt.Errorf("failed to umount directory %v: %v", mountedDirectory, err)
 	}
 	if _, err := exec.Command("sudo", "losetup", "-d", loopDevice).Output(); err != nil {
diff --git a/src/cmd/cos_image_analyzer/internal/utilities/logic_helper_test.go b/src/cmd/cos_image_analyzer/internal/utilities/logic_helper_test.go
index 7a2e687..98019dc 100644
--- a/src/cmd/cos_image_analyzer/internal/utilities/logic_helper_test.go
+++ b/src/cmd/cos_image_analyzer/internal/utilities/logic_helper_test.go
@@ -71,3 +71,25 @@
 		}
 	}
 }
+
+// test SliceToMapStr function
+func TestSliceToMapStr(t *testing.T) {
+	type test struct {
+		input []string
+		want  map[string]string
+	}
+
+	tests := []test{
+		{input: []string{"a", "b", "c", "d"}, want: map[string]string{"a": "", "b": "", "c": "", "d": ""}},
+		{input: []string{}, want: map[string]string{}},
+	}
+
+	for _, tc := range tests {
+		got := SliceToMapStr(tc.input)
+		for k, v := range tc.want {
+			if v != got[k] {
+				t.Fatalf("SliceToMapStr call expected: %v, got: %v", tc.want, got)
+			}
+		}
+	}
+}
diff --git a/src/cmd/cos_image_analyzer/main.go b/src/cmd/cos_image_analyzer/main.go
index 15d9a4a..e4894c2 100644
--- a/src/cmd/cos_image_analyzer/main.go
+++ b/src/cmd/cos_image_analyzer/main.go
@@ -42,7 +42,7 @@
 	}
 	imageDiff.BinaryDiff = binaryDiff
 
-	output, err := imageDiff.Formater(flagInfo)
+	output, err := imageDiff.Formater(image1.TempDir, image2.TempDir, flagInfo)
 	if err != nil {
 		return fmt.Errorf("failed to format image difference: %v", err)
 	}
@@ -68,43 +68,39 @@
 	return nil
 }
 
+func analyze(flagInfo *input.FlagInfo) error {
+	var image1, image2 *input.ImageInfo
+	defer func() {
+		if err := image1.Cleanup(); err != nil {
+			log.Printf("failed to clean up image %v: %v", flagInfo.Image1, err)
+		}
+		if err := image2.Cleanup(); err != nil {
+			log.Printf("failed to clean up image %v: %v", flagInfo.Image2, err)
+		}
+	}()
+	var err error
+	image1, image2, err = input.GetImages(flagInfo)
+	if err != nil {
+		return fmt.Errorf("failed to get images: %v", err)
+	}
+	if err := CallCosImageAnalyzer(image1, image2, flagInfo); err != nil {
+		return err
+	}
+	return nil
+}
+
 func main() {
 	if runtime.GOOS != "linux" {
 		fmt.Printf("Error: This is a Linux tool, can not run on %s", runtime.GOOS)
 	}
-
 	flagInfo, err := input.ParseFlags()
 	if err != nil {
 		log.Printf("failed to parse flags: %v\n", err)
 		os.Exit(1)
 	}
-
-	image1, image2, err := input.GetImages(flagInfo)
-	if err != nil {
-		log.Printf("failed to get images: %v", err)
-		if err := image1.Cleanup(); err != nil {
-			log.Printf("failed to clean up image %v: %v", flagInfo.Image1, err)
-		}
-		if err := image2.Cleanup(); err != nil {
-			log.Printf("failed to clean up image %v: %v", flagInfo.Image2, err)
-		}
-		os.Exit(1)
-	}
-
-	if err = CallCosImageAnalyzer(image1, image2, flagInfo); err != nil {
+	if err := analyze(flagInfo); err != nil {
 		log.Printf("%v\n", err)
-		if err := image1.Cleanup(); err != nil {
-			log.Printf("failed to clean up image %v: %v", flagInfo.Image1, err)
-		}
-		if err := image2.Cleanup(); err != nil {
-			log.Printf("failed to clean up image %v: %v", flagInfo.Image2, err)
-		}
 		os.Exit(1)
 	}
-	if err := image1.Cleanup(); err != nil {
-		log.Printf("failed to clean up image %v: %v", flagInfo.Image1, err)
-	}
-	if err := image2.Cleanup(); err != nil {
-		log.Printf("failed to clean up image %v: %v", flagInfo.Image2, err)
-	}
+	os.Exit(0)
 }