diff --git a/.gitignore b/.gitignore
index ade49e1eccbd37ac1df7a8c3efc9fb082e9c276a..0474a6e1a3ab4fc53c2c8a2e532e105d1ff87624 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@ node_modules
 .nyc_output
 coverage
 test-results.xml
+dump.lsif
+junit-report.xml
diff --git a/package-lock.json b/package-lock.json
index e668574a872c198d434446da897db3fbb4c15209..a498a5b219efbb4a2f13a544ce3c28807cc8a5bb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -314,6 +314,46 @@
 			"integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==",
 			"dev": true
 		},
+		"@sinonjs/commons": {
+			"version": "1.8.0",
+			"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz",
+			"integrity": "sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==",
+			"requires": {
+				"type-detect": "4.0.8"
+			}
+		},
+		"@sinonjs/fake-timers": {
+			"version": "6.0.1",
+			"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz",
+			"integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==",
+			"requires": {
+				"@sinonjs/commons": "^1.7.0"
+			}
+		},
+		"@sinonjs/formatio": {
+			"version": "5.0.1",
+			"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz",
+			"integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==",
+			"requires": {
+				"@sinonjs/commons": "^1",
+				"@sinonjs/samsam": "^5.0.2"
+			}
+		},
+		"@sinonjs/samsam": {
+			"version": "5.0.3",
+			"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.0.3.tgz",
+			"integrity": "sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ==",
+			"requires": {
+				"@sinonjs/commons": "^1.6.0",
+				"lodash.get": "^4.4.2",
+				"type-detect": "^4.0.8"
+			}
+		},
+		"@sinonjs/text-encoding": {
+			"version": "0.7.1",
+			"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
+			"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ=="
+		},
 		"@types/chai": {
 			"version": "4.2.11",
 			"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.11.tgz",
@@ -336,6 +376,21 @@
 			"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.13.tgz",
 			"integrity": "sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA=="
 		},
+		"@types/sinon": {
+			"version": "9.0.4",
+			"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.4.tgz",
+			"integrity": "sha512-sJmb32asJZY6Z2u09bl0G2wglSxDlROlAejCjsnor+LzBMz17gu8IU7vKC/vWDnv9zEq2wqADHVXFjf4eE8Gdw==",
+			"dev": true,
+			"requires": {
+				"@types/sinonjs__fake-timers": "*"
+			}
+		},
+		"@types/sinonjs__fake-timers": {
+			"version": "6.0.1",
+			"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz",
+			"integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==",
+			"dev": true
+		},
 		"aggregate-error": {
 			"version": "3.0.1",
 			"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
@@ -691,8 +746,7 @@
 		"diff": {
 			"version": "4.0.2",
 			"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
-			"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
-			"dev": true
+			"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
 		},
 		"dom-serializer": {
 			"version": "0.2.2",
@@ -1298,6 +1352,14 @@
 				"esprima": "^4.0.0"
 			}
 		},
+		"js2xmlparser": {
+			"version": "4.0.1",
+			"resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.1.tgz",
+			"integrity": "sha512-KrPTolcw6RocpYjdC7pL7v62e55q7qOMHvLX1UCLc5AAS8qeJ6nukarEJAF2KL2PZxlbGueEbINqZR2bDe/gUw==",
+			"requires": {
+				"xmlcreate": "^2.0.3"
+			}
+		},
 		"jsesc": {
 			"version": "2.5.2",
 			"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -1313,6 +1375,11 @@
 				"minimist": "^1.2.5"
 			}
 		},
+		"just-extend": {
+			"version": "4.1.0",
+			"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz",
+			"integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA=="
+		},
 		"locate-path": {
 			"version": "5.0.0",
 			"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -1334,6 +1401,11 @@
 			"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
 			"dev": true
 		},
+		"lodash.get": {
+			"version": "4.4.2",
+			"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+			"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
+		},
 		"log-symbols": {
 			"version": "3.0.0",
 			"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
@@ -1410,6 +1482,21 @@
 			"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
 			"dev": true
 		},
+		"matcher": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
+			"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
+			"requires": {
+				"escape-string-regexp": "^4.0.0"
+			},
+			"dependencies": {
+				"escape-string-regexp": {
+					"version": "4.0.0",
+					"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+					"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
+				}
+			}
+		},
 		"md5": {
 			"version": "2.2.1",
 			"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
@@ -1522,6 +1609,18 @@
 			"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
 			"dev": true
 		},
+		"nise": {
+			"version": "4.0.3",
+			"resolved": "https://registry.npmjs.org/nise/-/nise-4.0.3.tgz",
+			"integrity": "sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg==",
+			"requires": {
+				"@sinonjs/commons": "^1.7.0",
+				"@sinonjs/fake-timers": "^6.0.0",
+				"@sinonjs/text-encoding": "^0.7.1",
+				"just-extend": "^4.0.2",
+				"path-to-regexp": "^1.7.0"
+			}
+		},
 		"node-preload": {
 			"version": "0.2.1",
 			"resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz",
@@ -1765,6 +1864,21 @@
 			"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
 			"dev": true
 		},
+		"path-to-regexp": {
+			"version": "1.8.0",
+			"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+			"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+			"requires": {
+				"isarray": "0.0.1"
+			},
+			"dependencies": {
+				"isarray": {
+					"version": "0.0.1",
+					"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+					"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+				}
+			}
+		},
 		"pathval": {
 			"version": "1.1.0",
 			"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
@@ -1907,6 +2021,20 @@
 			"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
 			"dev": true
 		},
+		"sinon": {
+			"version": "9.0.2",
+			"resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.2.tgz",
+			"integrity": "sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A==",
+			"requires": {
+				"@sinonjs/commons": "^1.7.2",
+				"@sinonjs/fake-timers": "^6.0.1",
+				"@sinonjs/formatio": "^5.0.1",
+				"@sinonjs/samsam": "^5.0.3",
+				"diff": "^4.0.2",
+				"nise": "^4.0.1",
+				"supports-color": "^7.1.0"
+			}
+		},
 		"source-map": {
 			"version": "0.5.7",
 			"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -2052,8 +2180,7 @@
 		"type-detect": {
 			"version": "4.0.8",
 			"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
-			"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
-			"dev": true
+			"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="
 		},
 		"type-fest": {
 			"version": "0.8.1",
@@ -2199,6 +2326,11 @@
 			"integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=",
 			"dev": true
 		},
+		"xmlcreate": {
+			"version": "2.0.3",
+			"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.3.tgz",
+			"integrity": "sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ=="
+		},
 		"y18n": {
 			"version": "4.0.0",
 			"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
diff --git a/package.json b/package.json
index 766b5100e8af64eeba456e8dd1f5c0cb8c9ca7ed..aff1e5f8b52eaac998515cc579babe443ab4744a 100644
--- a/package.json
+++ b/package.json
@@ -34,11 +34,15 @@
 		"axios": "^0.19.2",
 		"chalk": "^4.1.0",
 		"commander": "^5.1.0",
-		"htmlparser2": "^4.1.0"
+		"htmlparser2": "^4.1.0",
+		"js2xmlparser": "^4.0.1",
+		"matcher": "^3.0.0",
+		"sinon": "^9.0.2"
 	},
 	"devDependencies": {
 		"@types/chai": "^4.2.11",
 		"@types/mocha": "^7.0.2",
+		"@types/sinon": "^9.0.4",
 		"chai": "^4.2.0",
 		"mocha": "^8.0.1",
 		"mocha-junit-reporter": "^2.0.0",
diff --git a/src/index.ts b/src/index.ts
index dcca8989fd6a8e5bb31be661b8263d94bb1416af..08d3ec18ccf0f7829bbae47a256b47d079b2a2f7 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,7 +1,7 @@
 import commander from "commander"
 import chalk from "chalk"
 import { Inspector, URLsMatchingSet, Config } from "./inspector"
-import { ConsoleReporter } from "./report"
+import { ConsoleReporter, JUnitReporter } from "./report"
 
 commander
 	.version("0.1.0") // TODO automatically
@@ -11,22 +11,47 @@ commander
 	.command("inspect <url>")
 	.description("Check links in the given URL")
 	.option("-r, --recursive", "recursively check all links in all URLs within supplied host", false)
+	.option("-v, --verbose", "log progress of checking URLs", false)
+	.option("-g, --get", "use GET request instead of HEAD", false)
 	.option("-t, --timeout <number>", "timeout in ms after which the link will be considered broken", (value: string, _) => parseInt(value), 2000)
-	.option("--ignore-prefixes <coma-separated-strings>", "prefix(es) to ignore (without ':'), like mailto: and tel:", (value: string, _) => value.split(","), ["mailto", "tel"])
+	.option("--ignore-prefixes <coma-separated-strings>", "prefix(es) to ignore (without ':'), like mailto: and tel:", (value: string, _) => value.split(","), ["javascript", "data", "mailto", "sms", "tel", "geo"])
 	.option("--accept-codes <coma-separated-numbers>", "HTTP response code(s) (beyond 200-299) to accept, like 999 for linkedin", (value: string, _) => value.split(",").map(code => parseInt(code)), [999])
+	.option("--reporters <coma-separated-strings>", "Reporters to use in processing the results (junit, console)", (value: string, _) => value.split(","), ["console"])
+	.option("--ignore-skipped", "Do not report skipped URLs", false)
 	.option("-s, --skip <globs>", "URLs to skip defined by globs, like '*linkedin*'", (value: string, previous: string[]) => previous.concat([value]), [])
 	.action(async (url: string, inspectObj) => {
 
+		try {
+			new URL(url)
+		} catch (e) {
+			console.error(chalk.red(`${url} does not look like valid URL (forgot http(s)?)`))
+			process.exit(1)
+		}
+
 		let inspector = new Inspector(new URLsMatchingSet(), {
 			acceptedCodes: inspectObj.acceptCodes as number[],
 			timeout: parseInt(inspectObj.timeout as string),
 			ignoredPrefixes: inspectObj.ignorePrefixes as string[],
-			skipURLs: inspectObj.skip as string[]
+			skipURLs: inspectObj.skip as string[],
+			verbose: inspectObj.verbose as boolean,
+			get: inspectObj.get as boolean,
+			ignoreSkipped: inspectObj.ignoreSkipped as boolean
 		})
-		let result = await inspector.processURL(new URL(url), inspectObj.recursive)
-		let success = result.report(new ConsoleReporter())
 
-		process.exit(success ? 0 : 1)
+		let result = await inspector.processURL(new URL(url), inspectObj.recursive as boolean)
+
+		for (const reporter of inspectObj.reporters as string[]) {
+			switch (reporter) {
+				case "junit":
+					result.report(new JUnitReporter())
+					break
+				case "console":
+					result.report(new ConsoleReporter())
+					break
+			}
+		}
+
+		process.exit(result.success() ? 0 : 1)
 	})
 
 if (!process.argv.slice(2).length) {
diff --git a/src/inspector.ts b/src/inspector.ts
index 1e6e1afb1eea6fe8922c7ff1ad9fc49125680cad..313f5a6b45cdb7efb40c00e65de71cdd26a90203 100644
--- a/src/inspector.ts
+++ b/src/inspector.ts
@@ -1,7 +1,7 @@
 import * as parser from "htmlparser2"
 import axios, { AxiosError } from "axios"
 import { Result, CheckStatus } from "./result";
-import { performance } from "perf_hooks"
+import { isMatch } from "matcher"
 
 export class Inspector {
 
@@ -10,64 +10,83 @@ export class Inspector {
 		private readonly config: Config
 	) { }
 
+	async timeout<T>(timeoutMs: number, promise: () => Promise<T>, failureMessage: string = "timeout"): Promise<T> {
+		let timeoutHandle: NodeJS.Timeout | undefined
+		const timeoutPromise = new Promise<never>((resolve, reject) => {
+			timeoutHandle = setTimeout(() => reject(new Error(failureMessage)), timeoutMs)
+		})
+
+		const result = await Promise.race([
+			promise(),
+			timeoutPromise
+		]);
+		clearTimeout(timeoutHandle!);
+		return result;
+	}
+
 	async processURL(originalUrl: URL, recursive: boolean): Promise<Result> {
 
-		let result = new Result();
-		let urlsToCheck: [string, string?][] = [[originalUrl.href, undefined]]
+		let result = new Result(this.config.ignoreSkipped);
+		// [url, GET, parent?]
+		let urlsToCheck: [string, boolean, string?][] = [[originalUrl.href, true, undefined]]
 
-		let processingRoutine = async (url: string, parent?: string) => {
+		let processingRoutine = async (url: string, useGet: boolean, parent?: string) => {
 
 			try {
-				url = parent ? new URL(url, parent).href : url
-
-				if (result.isChecked(url) || this.config.ignoredPrefixes.some(ext => url.startsWith(ext + ":"))) {
-					result.add({ url: url, status: CheckStatus.Skipped, duration: 0 }, parent)
+				url = parent ? new URL(url, parent).href : new URL(url).href
+				if (url.includes("#")) {
+					url = url.split("#")[0]
+				}
+				let shouldParse = url == originalUrl.href || (recursive && originalUrl.origin == new URL(url).origin)
+
+				if (
+					result.isChecked(url) ||
+					this.config.ignoredPrefixes.some(ext => url.startsWith(ext + ":")) ||
+					this.config.skipURLs.some(glob => url.includes(glob) || isMatch(url, glob))
+				) {
+					result.add({ url: url, status: CheckStatus.Skipped }, parent)
 				} else {
+					let urlToCheck = parent ? new URL(url, parent).href : url
 
 					const instance = axios.create()
-					instance.interceptors.request.use(config => {
-						config.headers["request-startTime"] = performance.now()
-						return config
-					})
-					instance.interceptors.response.use((response) => {
-						const start = response.config.headers["request-startTime"]
-						const end = performance.now()
-						response.headers["request-duration"] = end - start
-						return response
-					})
-					const response = await instance.get(parent ? new URL(url, parent).href : url, { timeout: this.config.timeout })
-					const duration = response.headers["request-duration"]
+					const response = useGet || shouldParse ?
+						await this.timeout(this.config.timeout, () => instance.get(urlToCheck)) :
+						await this.timeout(this.config.timeout, () => instance.head(urlToCheck))
 
 					let html = response.data as string
 
-					if (url == originalUrl.href || (recursive && originalUrl.origin == new URL(url).origin)) {
+					if (shouldParse) {
 
 						let discoveredURLs = this.extractURLs(html)
 
 						for (const discovered of discoveredURLs) {
-							urlsToCheck.push([discovered, url])
+							urlsToCheck.push([discovered, this.config.get, url])
 						}
 					}
 
-					result.add({ url: url, status: CheckStatus.OK, duration: duration }, parent)
+					result.add({ url: url, status: CheckStatus.OK }, parent)
 				}
 
 			} catch (exception) {
 				const error: AxiosError = exception;
 
-				if ((exception.message as string).includes("timeout")) {
-					result.add({ url: url, status: CheckStatus.Timeout, duration: this.config.timeout }, parent)
-				} else if (!error.response) {
-					result.add({ url: url, status: CheckStatus.GenericError, duration: 0 }, parent)
+				// if HEAD was used, retry with GET
+				if (!useGet) {
+					urlsToCheck.push([url, true, parent])
 				} else {
-					const duration = performance.now() - error.response.config.headers["request-startTime"]
-
-					if (this.config.acceptedCodes.some(code => code == error.response?.status)) {
-						result.add({ url: url, status: CheckStatus.OK, duration: duration }, parent)
+					if ((exception.message as string).includes("timeout")) {
+						result.add({ url: url, status: CheckStatus.Timeout }, parent)
+					} else if (!error.response) {
+						result.add({ url: url, status: CheckStatus.GenericError }, parent)
 					} else {
-						result.add({ url: url, status: CheckStatus.NonSuccessCode, message: `${error.response.status}`, duration: duration }, parent)
+						if (this.config.acceptedCodes.some(code => code == error.response?.status)) {
+							result.add({ url: url, status: CheckStatus.OK }, parent)
+						} else {
+							result.add({ url: url, status: CheckStatus.NonSuccessCode, message: `${error.response.status}` }, parent)
+						}
 					}
 				}
+
 			}
 
 		}
@@ -76,15 +95,16 @@ export class Inspector {
 
 		while (urlsToCheck.length > 0) {
 
-			let [url, parent] = urlsToCheck.pop()!
+			let [url, useGet, parent] = urlsToCheck.pop()!
 
-			promises.push(processingRoutine(url, parent))
+			promises.push(processingRoutine(url, useGet, parent))
 
 			if (urlsToCheck.length == 0) {
 				await Promise.all(promises)
 			}
 		}
 
+		console.log()
 		return result
 	}
 
@@ -117,13 +137,17 @@ export class Config {
 	timeout: number = 2000
 	ignoredPrefixes: string[] = ["mailto", "tel"]
 	skipURLs: string[] = []
+	verbose: boolean = false
+	get: boolean = false
+	ignoreSkipped: boolean = false
 }
 
 export enum URLMatchingRule {
 	AHRef = "<a href>",
 	ScriptSrc = "<script src>",
 	LinkHref = "<link href>",
-	ImgSrc = "<img src>"
+	ImgSrc = "<img src>",
+	IFrameSrc = "<iframe src>"
 }
 
 export class URLsMatchingSet {
@@ -157,6 +181,11 @@ export class URLsMatchingSet {
 						return attributes.src
 					}
 					break;
+				case URLMatchingRule.IFrameSrc:
+					if (name === "iframe" && "src" in attributes) {
+						return attributes.src
+					}
+					break;
 				default:
 					throw new Error(`unknown rule: ${rule}`);
 			}
diff --git a/src/report.ts b/src/report.ts
index 5f8da3eb535dd22b00bbe79a8636f6d3086f39fb..b95bb6f1dc7461cf5ca8859d215ab13631df00e2 100644
--- a/src/report.ts
+++ b/src/report.ts
@@ -1,11 +1,123 @@
-import { ResultItem, CheckStatus } from "./result";
-import chalk from "chalk";
+import { ResultItem, CheckStatus } from "./result"
+import chalk from "chalk"
+import { parse } from "js2xmlparser"
+
+import fs from "fs";
 
 export interface IReporter {
-	process(pages: Map<string, ResultItem[]>): boolean
+	process(pages: Map<string, ResultItem[]>): void
 }
 
-// ConsoleReporter
+/**
+ *	testsuite = [
+ *		{
+ *			"@": {
+ *				name: "parent URL",
+ *				tests: totalTests
+ *				failures: broken,
+ *				skipped: skipped
+ *			}
+ *			"testcase" = [
+ *				{
+ *					"@": {
+ *						name: URL,
+ *					},
+ *					error?: {
+ *						"@": {
+ *							message: "error message"
+ *						}
+ *					},
+ *					skipped?: {}
+ *				}
+ *			]
+ *		}
+ *	]
+ */
+export class JUnitReporter implements IReporter {
+
+	constructor(readonly toFile: boolean = true) { }
+
+	process(pages: Map<string, ResultItem[]>): void {
+
+		let junitObject: any[] = []
+
+		for (const page of pages) {
+
+			let testsuite: any = {
+				testcase: []
+			}
+
+			let skipped = 0
+			let oks = 0
+			let broken = 0
+
+			for (const check of page[1]) {
+
+				let testcase: any = {
+					"@": {
+						name: check.url,
+						classname: page[0],
+						time: "0.0000"
+					}
+				}
+
+				switch (check.status) {
+					case CheckStatus.NonSuccessCode:
+						testcase["failure"] = {
+							"@": {
+								message: check.message
+							}
+						}
+						broken++
+						break
+					case CheckStatus.GenericError:
+						testcase["failure"] = {
+							"@": {
+								message: "Unknown error"
+							}
+						}
+						broken++
+						break
+					case CheckStatus.Timeout:
+						testcase["failure"] = {
+							"@": {
+								message: "Timeout"
+							}
+						}
+						broken++
+						break
+					case CheckStatus.Skipped:
+						testcase["skipped"] = {}
+						skipped++
+						break
+					case CheckStatus.OK:
+						oks++
+						break
+				}
+
+				testsuite.testcase.push(testcase)
+			}
+
+			testsuite["@"] = {
+				name: page[0],
+				tests: oks + skipped + broken,
+				failures: broken,
+				skipped: skipped,
+				time: "0.0000"
+			}
+
+			junitObject.push(testsuite)
+		}
+
+		let junitXml = parse("testsuites", { testsuite: junitObject })
+		if (this.toFile) {
+			fs.writeFileSync("junit-report.xml", junitXml)
+		} else {
+			console.log(junitXml)
+		}
+	}
+
+}
 
 export class ConsoleReporter implements IReporter {
 
@@ -38,7 +150,7 @@ export class ConsoleReporter implements IReporter {
 		}
 	}
 
-	process(pages: Map<string, ResultItem[]>): boolean {
+	process(pages: Map<string, ResultItem[]>) {
 
 		let allSkipped = 0
 		let allOks = 0
@@ -75,8 +187,6 @@ export class ConsoleReporter implements IReporter {
 			allBroken += broken
 		}
 		this.printTotals(allOks, allSkipped, allBroken, false)
-
-		return allBroken == 0
 	}
 
 }
diff --git a/src/result.ts b/src/result.ts
index ea1c25d4cc24037ba698d6a9f842c025d7794ea4..1123e5bbf6f241f0e8883bfd1f7e8b8706cda663 100644
--- a/src/result.ts
+++ b/src/result.ts
@@ -3,9 +3,21 @@ import { IReporter } from "./report"
 export class Result {
 	private pages = new Map<string, ResultItem[]>()
 	private checkedUrls = new Set<string>()
+	private addedCount = 0
+	private atLeastOneBroken = false
+
+	constructor(readonly ignoreSkipped: boolean) { }
 
 	public add(completedCheck: ResultItem, parent: string = "original request") {
-		// console.log(`${completedCheck.url} : ${completedCheck.status} ${completedCheck.message ? completedCheck.message : ""}`) // TODO
+		if (completedCheck.status == CheckStatus.Skipped && this.ignoreSkipped) {
+			return
+		}
+
+		if (this.addedCount > 0 && this.addedCount % 80 == 0) {
+			process.stdout.write("\n")
+		}
+		process.stdout.write(completedCheck.status == CheckStatus.OK || completedCheck.status == CheckStatus.Skipped ? "." : "x")
+		this.addedCount++
 
 		if (this.pages.has(parent)) {
 			this.pages.get(parent)?.push(completedCheck)
@@ -13,6 +25,14 @@ export class Result {
 			this.pages.set(parent, [completedCheck])
 		}
 		this.checkedUrls.add(completedCheck.url)
+
+		if (
+			completedCheck.status == CheckStatus.GenericError ||
+			completedCheck.status == CheckStatus.Timeout ||
+			completedCheck.status == CheckStatus.NonSuccessCode
+		) {
+			this.atLeastOneBroken = true
+		}
 	}
 
 	public isChecked(url: string): boolean {
@@ -27,15 +47,18 @@ export class Result {
 		return count
 	}
 
-	public report<ReporterT extends IReporter>(reporter: ReporterT): boolean {
-		return reporter.process(this.pages)
+	public report<ReporterT extends IReporter>(reporter: ReporterT): void {
+		reporter.process(this.pages)
+	}
+
+	public success() {
+		return !this.atLeastOneBroken
 	}
 }
 
 export class ResultItem {
 	public url = ""
 	public status = CheckStatus.OK
-	public duration = 0
 	public message?: string
 }
 
diff --git a/test/extract-urls.ts b/test/extract-urls.ts
index d56611a04fa436cac6f84060349bc9bbf56dd08d..17d6f11ddfe763e8765e5952ea96bf6e3eca5647 100644
--- a/test/extract-urls.ts
+++ b/test/extract-urls.ts
@@ -26,6 +26,11 @@ describe("extractURLs", () => {
 		expect(result).to.eql(new Set([url]))
 	});
 
+	it("works for <iframe src=...>", () => {
+		const result = new Inspector(new URLsMatchingSet(URLMatchingRule.IFrameSrc), new Config()).extractURLs(`<html><iframe src="${url}">Text</iframe></html>`)
+		expect(result).to.eql(new Set([url]))
+	});
+
 	it("works for many rules", () => {
 		const result = new Inspector(new URLsMatchingSet(), new Config())
 			.extractURLs(
diff --git a/test/process-url.ts b/test/process-url.ts
index 0f1f852add93e3d8322aad3206fcfd859ec6e2f6..c397ff443ea1503e0a0cbf9bb12d3fcabfa58e11 100644
--- a/test/process-url.ts
+++ b/test/process-url.ts
@@ -1,7 +1,8 @@
 import { Inspector, URLsMatchingSet, URLMatchingRule, Config } from "../src/inspector"
 import { expect, assert } from "chai";
 import "mocha";
-import { ConsoleReporter } from "../src/report";
+import { ConsoleReporter, JUnitReporter } from "../src/report";
+import sinon from "sinon"
 
 describe("processURL", function () {
 
@@ -9,6 +10,11 @@ describe("processURL", function () {
 
 	const validURL = new URL("https://dbogatov.org")
 
+	before(function () {
+		sinon.stub(console, "log")
+		sinon.stub(process.stdout, "write")
+	});
+
 	it("processes non-recursive", async () => {
 		const result = await new Inspector(new URLsMatchingSet(), new Config()).processURL(validURL, false)
 
@@ -21,6 +27,7 @@ describe("processURL", function () {
 		const result = await new Inspector(new URLsMatchingSet(), new Config()).processURL(validURL, true)
 
 		result.report(new ConsoleReporter())
+		result.report(new JUnitReporter(false))
 
 		// assert(result.length == 1)
 		// assert(result[0].url === validURL.href)