diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cf6d37f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "bundler" + directory: "/" + schedule: + interval: "daily" + labels: + - "dependency::update" diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml new file mode 100644 index 0000000..9bddc11 --- /dev/null +++ b/.github/workflows/ruby.yml @@ -0,0 +1,33 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake +# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby + +name: Ruby + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ['3.1', '3.0', '2.7', '2.6'] + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, + # change this to (see https://github.com/ruby/setup-ruby#versioning): + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run lint + run: bundle exec rubocop diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68ebb2e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.bundle +vendor/ +*.gem \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..9f62526 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,9 @@ +AllCops: + TargetRubyVersion: 2.6 + NewCops: enable + SuggestExtensions: false +Layout/HashAlignment: + EnforcedHashRocketStyle: table +Layout/LineLength: + Exclude: + - '*.gemspec' \ No newline at end of file diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..0b2d858 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 3.1.2 diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..e69de29 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..00801a5 --- /dev/null +++ b/Gemfile @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gemspec + +group :runtime, :cli do + # none +end + +group :development, :install do + gem 'bundler', '~> 2.1' +end + +group :development, :test do + # none +end + +group :development, :lint do + gem 'rubocop', '~> 1.28' +end + +group :development, :docs do + # none +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..8d36283 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,41 @@ +PATH + remote: . + specs: + bqm (1.0.0) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + json (2.6.2) + parallel (1.22.1) + parser (3.1.2.1) + ast (~> 2.4.1) + rainbow (3.1.1) + regexp_parser (2.6.0) + rexml (3.2.5) + rubocop (1.37.1) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.1.2.1) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.23.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.23.0) + parser (>= 3.1.1.0) + ruby-progressbar (1.11.0) + unicode-display_width (2.3.0) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + bqm! + bundler (~> 2.1) + rubocop (~> 1.28) + +BUNDLED WITH + 2.3.7 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f287853 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Alexandre ZANNI at ACCEIS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5ba7b2 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# BQM (Bloodhound Query Merger) + +![BQM logo](assets/logo-bqm.png) + +> Tool to deduplicate custom BloudHound queries from different datasets and merge them in one `customqueries.json` file. + +## Why? + +[BloodHound][bh] allows you to store custom queries in `~/.config/bloodhound/customqueries.json`. Most pentester are then downloading a custom queries file from an external project. **The issue?** There are several projects offering very good queries files but they are all very different and complementary and BloodHound supports only one custom queries file. **The solution?** What if a tool would index all custom queries files, download them for you, remove duplicate queries and merge them all in one file you can use in BloodHound? That's what BQM offers, no more query file compromise, more AD compromise! + +## Install + +No install, just clone the repository and run! No dependencies, just pure Ruby. + +```bash +git clone https://github.com/Acceis/bqm.git && cd bqm +ruby bqm.rb -h +``` + +### Gem + +Coming soon + +### BlackArch + +Coming soon + +### AUR + +Coming soon + +## Usage + +``` +Usage: bqm.rb [options] + -o, --output-path PATH Path where to store the query file + +Example: bqm.rb -o ~/.config/bloodhound/customqueries.json +``` + +## Datasets + +Datasets used by BQM are referenced in `data/query-sets.json`. They are coming from the following projects: + +- [ly4k/Certipy](https://github.com/ly4k/Certipy) +- [CompassSecurity/BloodHoundQueries](https://github.com/CompassSecurity/BloodHoundQueries) +- [hausec/Bloodhound-Custom-Queries](https://github.com/hausec/Bloodhound-Custom-Queries) +- [awsmhacks/awsmBloodhoundCustomQueries](https://github.com/awsmhacks/awsmBloodhoundCustomQueries) +- [porterhau5/BloodHound-Owned](https://github.com/porterhau5/BloodHound-Owned) +- [ZephrFish/Bloodhound-CustomQueries](https://github.com/ZephrFish/Bloodhound-CustomQueries) +- [Scoubi/BloodhoundAD-Queries](https://github.com/Scoubi/BloodhoundAD-Queries) + +## Author + +Made by Alexandre ZANNI ([@noraj](https://pwn.by/noraj/)) for [ACCEIS](https://www.acceis.fr/). + +## Credits + +Logo made with [DesignEvo](https://www.designevo.com/). + +[bh]:https://github.com/BloodHoundAD/BloodHound \ No newline at end of file diff --git a/assets/logo-bqm.png b/assets/logo-bqm.png new file mode 100644 index 0000000..a2f041e Binary files /dev/null and b/assets/logo-bqm.png differ diff --git a/bqm.gemspec b/bqm.gemspec new file mode 100644 index 0000000..d0129bc --- /dev/null +++ b/bqm.gemspec @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +Gem::Specification.new do |s| + s.name = 'bqm' + s.version = '1.0.0' + s.platform = Gem::Platform::RUBY + s.summary = 'HAsh IdenTifIer' + s.description = 'Deduplicate custom BloudHound queries from different datasets and merge them in one customqueries.json file.' + s.authors = ['Alexandre ZANNI'] + s.email = 'alexandre.zanni@europe.com' + s.homepage = 'https://github.com/Acceis/bqm' + s.license = 'MIT' + + s.bindir = '.' + s.files = Dir['data/*.json'] + ['bqm.rb', 'LICENSE'] + s.executables = ['bqm.rb'] + + s.metadata = { + 'bug_tracker_uri' => 'https://github.com/Acceis/bqm/issues', + # 'changelog_uri' => 'https://github.com/Acceis/bqm/blob/master/docs/CHANGELOG.md', + # 'documentation_uri' => 'https://github.com/Acceis/bqm', + 'homepage_uri' => 'https://github.com/Acceis/bqm', + 'source_code_uri' => 'https://github.com/Acceis/bqm/', + 'rubygems_mfa_required' => 'true' + } + + s.required_ruby_version = ['>= 2.6.0', '< 3.2'] +end diff --git a/bqm.rb b/bqm.rb new file mode 100755 index 0000000..1d0df61 --- /dev/null +++ b/bqm.rb @@ -0,0 +1,73 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'json' +require 'net/http' + +def find_dataset + source_file = 'query-sets.json' + source_file_paths = ['./data', '/usr/share/bqm/data', '~/.local/share/bqm/data'] + source_file_paths.each do |path| + candidate = "#{path}/#{source_file}" + return candidate if File.file?(candidate) && File.readable?(candidate) + end + raise IOError, "The dataset file #{source_file} does not exist or is unreadable." +end + +def merge(source) + src = JSON.load_file(source) + queries = [] + src['sets'].each do |s| + customqueries = Net::HTTP.get(URI(s)) + data = JSON.parse(customqueries) + queries += data['queries'] + end + queries +end + +# Query class just for the sake of having custom comparison +class BQMquery + attr_accessor :data + + def initialize(query) + @data = query + end + + def eql?(other) + @data['name'].eql?(other.data['name']) && @data['queryList'].eql?(other.data['queryList']) + end + + def hash + @data.hash + end +end + +def deduplicate(data) + data.map { |x| BQMquery.new(x) }.uniq +end + +if __FILE__ == $PROGRAM_NAME + source = find_dataset + data = merge(source) + queries = deduplicate(data).map(&:data) + + require 'optparse' + options = {} + OptionParser.new do |parser| + parser.banner = 'Usage: bqm.rb [options]' + + parser.on('-o', '--output-path PATH', 'Path where to store the query file') + parser.separator '' + parser.separator 'Example: bqm.rb -o ~/.config/bloodhound/customqueries.json' + end.parse!(into: options) + + out = options[:'output-path'] + if out + File.open(out, 'w') do |file| + json = JSON.pretty_generate({ 'queries' => queries }) + file.write(json) + end + else + puts 'Help: bqm.rb -h' + end +end diff --git a/data/query-sets.json b/data/query-sets.json new file mode 100644 index 0000000..4cf1c6b --- /dev/null +++ b/data/query-sets.json @@ -0,0 +1,11 @@ +{ + "sets": [ + "https://raw.githubusercontent.com/ly4k/Certipy/main/customqueries.json", + "https://raw.githubusercontent.com/CompassSecurity/BloodHoundQueries/master/customqueries.json", + "https://raw.githubusercontent.com/hausec/Bloodhound-Custom-Queries/master/customqueries.json", + "https://raw.githubusercontent.com/awsmhacks/awsmBloodhoundCustomQueries/master/customqueries.json", + "https://raw.githubusercontent.com/porterhau5/BloodHound-Owned/master/customqueries.json", + "https://raw.githubusercontent.com/ZephrFish/Bloodhound-CustomQueries/main/customqueries.json", + "https://raw.githubusercontent.com/Scoubi/BloodhoundAD-Queries/master/customqueries.json" + ] +} \ No newline at end of file