diff --git a/scanners/subfinder/README.md b/scanners/subfinder/README.md index 44b8feab93..f65cc3584d 100644 --- a/scanners/subfinder/README.md +++ b/scanners/subfinder/README.md @@ -65,6 +65,7 @@ Kubernetes: `>=v1.11.0-0` |-----|------|---------|-------------| | cascadingRules.enabled | bool | `false` | Enables or disables the installation of the default cascading rules for this scanner | | imagePullSecrets | list | `[]` | Define imagePullSecrets when a private registry is used (see: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/) | +| includeTargetDomain | bool | false | Include target domain in findings | | parser.affinity | object | `{}` | Optional affinity settings that control how the parser job is scheduled (see: https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/) | | parser.env | list | `[]` | Optional environment variables mapped into each parseJob (see: https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/) | | parser.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy. One of Always, Never, IfNotPresent. Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. More info: https://kubernetes.io/docs/concepts/containers/images#updating-images | diff --git a/scanners/subfinder/parser/__snapshots__/parser.test.js.snap b/scanners/subfinder/parser/__snapshots__/parser.test.js.snap index 901da3c7c9..840f42518e 100644 --- a/scanners/subfinder/parser/__snapshots__/parser.test.js.snap +++ b/scanners/subfinder/parser/__snapshots__/parser.test.js.snap @@ -157,3 +157,450 @@ exports[`should properly parse subfinder json file with ip output 1`] = ` `; exports[`should properly parse empty json file 1`] = `[]`; + +exports[`should properly parse subfinder json file and add target domain to findings with param -d 1`] = ` +[ + { + "attributes": { + "domain": "example.com", + "hostname": "releases.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain releases.example.com", + "identified_at": null, + "location": "releases.example.com", + "name": "releases.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "myap.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain myap.example.com", + "identified_at": null, + "location": "myap.example.com", + "name": "myap.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "not-exist.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain not-exist.example.com", + "identified_at": null, + "location": "not-exist.example.com", + "name": "not-exist.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "new-client.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain new-client.example.com", + "identified_at": null, + "location": "new-client.example.com", + "name": "new-client.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "op.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain op.example.com", + "identified_at": null, + "location": "op.example.com", + "name": "op.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "mall.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain mall.example.com", + "identified_at": null, + "location": "mall.example.com", + "name": "mall.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "cdn1.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain cdn1.example.com", + "identified_at": null, + "location": "cdn1.example.com", + "name": "cdn1.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "xn--rksmrgs-5wao1o.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain xn--rksmrgs-5wao1o.example.com", + "identified_at": null, + "location": "xn--rksmrgs-5wao1o.example.com", + "name": "xn--rksmrgs-5wao1o.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "example.com", + "ip_address": null, + "ip_addresses": [], + "source": "parser", + }, + "category": "Subdomain", + "description": "Found subdomain example.com", + "identified_at": null, + "location": "example.com", + "name": "example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, +] +`; + +exports[`should properly parse subfinder json file and add target domain to findings with param -domain 1`] = ` +[ + { + "attributes": { + "domain": "example.com", + "hostname": "releases.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain releases.example.com", + "identified_at": null, + "location": "releases.example.com", + "name": "releases.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "myap.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain myap.example.com", + "identified_at": null, + "location": "myap.example.com", + "name": "myap.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "not-exist.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain not-exist.example.com", + "identified_at": null, + "location": "not-exist.example.com", + "name": "not-exist.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "new-client.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain new-client.example.com", + "identified_at": null, + "location": "new-client.example.com", + "name": "new-client.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "op.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain op.example.com", + "identified_at": null, + "location": "op.example.com", + "name": "op.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "mall.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain mall.example.com", + "identified_at": null, + "location": "mall.example.com", + "name": "mall.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "cdn1.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain cdn1.example.com", + "identified_at": null, + "location": "cdn1.example.com", + "name": "cdn1.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "xn--rksmrgs-5wao1o.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain xn--rksmrgs-5wao1o.example.com", + "identified_at": null, + "location": "xn--rksmrgs-5wao1o.example.com", + "name": "xn--rksmrgs-5wao1o.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "example.com", + "ip_address": null, + "ip_addresses": [], + "source": "parser", + }, + "category": "Subdomain", + "description": "Found subdomain example.com", + "identified_at": null, + "location": "example.com", + "name": "example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, +] +`; + +exports[`should properly parse subfinder json file and add target domain to findings with param --domain= 1`] = ` +[ + { + "attributes": { + "domain": "example.com", + "hostname": "releases.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain releases.example.com", + "identified_at": null, + "location": "releases.example.com", + "name": "releases.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "myap.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain myap.example.com", + "identified_at": null, + "location": "myap.example.com", + "name": "myap.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "not-exist.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain not-exist.example.com", + "identified_at": null, + "location": "not-exist.example.com", + "name": "not-exist.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "new-client.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain new-client.example.com", + "identified_at": null, + "location": "new-client.example.com", + "name": "new-client.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "op.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain op.example.com", + "identified_at": null, + "location": "op.example.com", + "name": "op.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "mall.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain mall.example.com", + "identified_at": null, + "location": "mall.example.com", + "name": "mall.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "cdn1.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain cdn1.example.com", + "identified_at": null, + "location": "cdn1.example.com", + "name": "cdn1.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "xn--rksmrgs-5wao1o.example.com", + "ip_address": null, + "ip_addresses": [], + "source": "alienvault", + }, + "category": "Subdomain", + "description": "Found subdomain xn--rksmrgs-5wao1o.example.com", + "identified_at": null, + "location": "xn--rksmrgs-5wao1o.example.com", + "name": "xn--rksmrgs-5wao1o.example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, + { + "attributes": { + "domain": "example.com", + "hostname": "example.com", + "ip_address": null, + "ip_addresses": [], + "source": "parser", + }, + "category": "Subdomain", + "description": "Found subdomain example.com", + "identified_at": null, + "location": "example.com", + "name": "example.com", + "osi_layer": "NETWORK", + "severity": "INFORMATIONAL", + }, +] +`; diff --git a/scanners/subfinder/parser/parser.js b/scanners/subfinder/parser/parser.js index 7bd8135618..2afdfc93ce 100644 --- a/scanners/subfinder/parser/parser.js +++ b/scanners/subfinder/parser/parser.js @@ -2,32 +2,89 @@ // // SPDX-License-Identifier: Apache-2.0 -export async function parse(fileContent) { - if (!fileContent) - return []; +const DOMAIN_FLAGS = ["-d", "-domain", "--domain"]; + +export async function parse( + fileContent, + scan, + includeTargetDomain = process.env["INCLUDE_TARGET_DOMAIN"]?.toLowerCase() == + "true", +) { + if (!fileContent && !includeTargetDomain) return []; const targets = parseResultFile(fileContent); - return transformToFindings(targets); + const findings = transformToFindings(targets); + + const domain = includeTargetDomain + ? extractDomainFromArgs(scan.spec.parameters) + : null; + if (domain) { + findings.push(getTargetDomainFinding(domain)); + } + + return findings; } -function transformToFindings(targets) { - return targets.map((item) => ({ - "name": item.host, - "identified_at": null, - "description": `Found subdomain ${item.host}`, - "category": "Subdomain", - "location": item.host, - "osi_layer": "NETWORK", - "severity": "INFORMATIONAL", - "attributes": { - "domain": item.input, - "hostname": item.host, - "ip_address": item?.ip || null, - "ip_addresses": [item?.ip || null].filter(Boolean), - "source": item.source, +function extractDomainFromArgs(args) { + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + // Check for --domain=value or --domain="value" format + for (const flag of DOMAIN_FLAGS) { + if (arg.startsWith(`${flag}=`)) { + const value = arg.slice(flag.length + 1); + // Remove surrounding quotes if present + return value + .replace(/^["']/, "") // Remove leading quote + .replace(/["']$/, ""); // Remove trailing quote } } - )); + + // Check for -d value, -domain value, --domain value format + if (DOMAIN_FLAGS.includes(arg) && i + 1 < args.length) { + return args[i + 1]; + } + } + + return null; +} + +function getTargetDomainFinding(domain) { + return { + name: domain, + identified_at: null, + description: `Found subdomain ${domain}`, + category: "Subdomain", + location: domain, + osi_layer: "NETWORK", + severity: "INFORMATIONAL", + attributes: { + domain: domain, + hostname: domain, + ip_address: null, + ip_addresses: [], + source: "parser", + }, + }; +} + +function transformToFindings(targets) { + return targets.map((item) => ({ + name: item.host, + identified_at: null, + description: `Found subdomain ${item.host}`, + category: "Subdomain", + location: item.host, + osi_layer: "NETWORK", + severity: "INFORMATIONAL", + attributes: { + domain: item.input, + hostname: item.host, + ip_address: item?.ip || null, + ip_addresses: [item?.ip || null].filter(Boolean), + source: item.source, + }, + })); } /** @@ -35,7 +92,8 @@ function transformToFindings(targets) { * @param {*} fileContent */ function parseResultFile(fileContent) { - return fileContent.trim() - .split('\n') - .map(line => JSON.parse(line)); + return fileContent + .trim() + .split("\n") + .map((line) => JSON.parse(line)); } diff --git a/scanners/subfinder/parser/parser.test.js b/scanners/subfinder/parser/parser.test.js index 9e1c85abc2..90c761b122 100644 --- a/scanners/subfinder/parser/parser.test.js +++ b/scanners/subfinder/parser/parser.test.js @@ -41,3 +41,81 @@ test("should properly parse empty json file", async () => { expect(validateParser(findings)).toBeUndefined(); expect(findings).toMatchSnapshot(); }); + +test("should properly parse subfinder json file and add target domain to findings with param -d", async () => { + const scan = { + spec: { + scanType: "subfinder", + parameters: ["-timeout", "1", "-d", "example.com"], + }, + metadata: { + annotations: { + "metadata.scan.securecodebox.io/subfinder": + "https://github.com/secureCodeBox/secureCodeBox", + }, + }, + }; + + const fileContent = await readFile( + __dirname + "/__testFiles__/passive_scan_without_ip_example.com.jsonl", + { + encoding: "utf8", + }, + ); + const findings = await parse(fileContent, scan, "true"); + // validate findings + expect(validateParser(findings)).toBeUndefined(); + expect(findings).toMatchSnapshot(); +}); + +test("should properly parse subfinder json file and add target domain to findings with param -domain", async () => { + const scan = { + spec: { + scanType: "subfinder", + parameters: ["-timeout", "1", "-domain", "example.com"], + }, + metadata: { + annotations: { + "metadata.scan.securecodebox.io/subfinder": + "https://github.com/secureCodeBox/secureCodeBox", + }, + }, + }; + + const fileContent = await readFile( + __dirname + "/__testFiles__/passive_scan_without_ip_example.com.jsonl", + { + encoding: "utf8", + }, + ); + const findings = await parse(fileContent, scan, "true"); + // validate findings + expect(validateParser(findings)).toBeUndefined(); + expect(findings).toMatchSnapshot(); +}); + +test("should properly parse subfinder json file and add target domain to findings with param --domain=", async () => { + const scan = { + spec: { + scanType: "subfinder", + parameters: ["-timeout", "1", "--domain=example.com"], + }, + metadata: { + annotations: { + "metadata.scan.securecodebox.io/subfinder": + "https://github.com/secureCodeBox/secureCodeBox", + }, + }, + }; + + const fileContent = await readFile( + __dirname + "/__testFiles__/passive_scan_without_ip_example.com.jsonl", + { + encoding: "utf8", + }, + ); + const findings = await parse(fileContent, scan, "true"); + // validate findings + expect(validateParser(findings)).toBeUndefined(); + expect(findings).toMatchSnapshot(); +}); diff --git a/scanners/subfinder/templates/subfinder-parse-definition.yaml b/scanners/subfinder/templates/subfinder-parse-definition.yaml index 66ed1a34f8..9af468d3ce 100644 --- a/scanners/subfinder/templates/subfinder-parse-definition.yaml +++ b/scanners/subfinder/templates/subfinder-parse-definition.yaml @@ -10,7 +10,10 @@ spec: image: "{{ .Values.parser.image.repository }}:{{ .Values.parser.image.tag | default .Chart.Version }}" imagePullPolicy: {{.Values.parser.image.pullPolicy}} ttlSecondsAfterFinished: {{.Values.parser.ttlSecondsAfterFinished}} - env: {{- toYaml .Values.parser.env | nindent 4}} + env: + {{- $env := .Values.parser.env | default (list) }} + {{- $env = append $env (dict "name" "INCLUDE_TARGET_DOMAIN" "value" (.Values.includeTargetDomain | toString)) }} + {{- toYaml $env | nindent 4 }} scopeLimiterAliases: {{- toYaml .Values.parser.scopeLimiterAliases | nindent 4}} affinity: {{- toYaml .Values.parser.affinity | nindent 4}} diff --git a/scanners/subfinder/values.yaml b/scanners/subfinder/values.yaml index b3dea8c4de..393b04e84f 100644 --- a/scanners/subfinder/values.yaml +++ b/scanners/subfinder/values.yaml @@ -5,6 +5,9 @@ # -- Define imagePullSecrets when a private registry is used (see: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/) imagePullSecrets: [] +# includeTargetDomain -- include target domain in findings +includeTargetDomain: false + parser: image: # parser.image.repository -- Parser image repository @@ -75,7 +78,6 @@ scanner: - name: subfinder-config emptyDir: {} - # scanner.extraVolumeMounts -- Optional VolumeMounts mapped into each scanJob (see: https://kubernetes.io/docs/concepts/storage/volumes/) extraVolumeMounts: - name: subfinder-config @@ -85,10 +87,7 @@ scanner: extraContainers: [] # scanner.podSecurityContext -- Optional securityContext set on scanner pod (see: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) - podSecurityContext: - { - runAsUser: 10001 - } + podSecurityContext: {runAsUser: 10001} # scanner.securityContext -- Optional securityContext set on scanner container (see: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) securityContext: