msid 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +13 -141
- data/lib/msid/source.rb +59 -0
- data/lib/msid/sources/darwin.rb +78 -0
- data/lib/msid/sources/linux.rb +73 -0
- data/lib/msid/sources/windows.rb +86 -0
- data/lib/msid/version.rb +1 -1
- data/lib/msid.rb +32 -216
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a66c2777ff91a9d45dadccdfeed8d8428ce55b153ab5dfb47fbe933042ac918c
|
|
4
|
+
data.tar.gz: 417965adb88e1fb02a1907f786d7fe09119958ed5ce5707e010399eb35c9f334
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a427b6baad02240f442fea21a66ec814404be918c2070c0006521e91387ad4b89f8ec26a649f4bb4bf2e157f40814b243e45855571adaab3822b8ac487a24ed3
|
|
7
|
+
data.tar.gz: 2cd36016dafbf2a3da4e0f9a421e23bc29d62ad858de5dd0ca96fd811a6410249c2a72d9ee0b6d408c782fe3fae2bb1a47aa021f0baf0e12117cc3f402b7ca91
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -1,169 +1,41 @@
|
|
|
1
1
|
# msid
|
|
2
2
|
|
|
3
|
-
`msid`
|
|
3
|
+
`msid` generates a **unique, secure, and consistent machine fingerprint** for Ruby applications. It gathers hardware identifiers (CPU, Mac Address, Disk UUIDs, etc.) to create a specific SHA-256 hash.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## How It Works
|
|
8
|
-
|
|
9
|
-
`msid` collects the following system information:
|
|
10
|
-
- Hostname
|
|
11
|
-
- All MAC addresses
|
|
12
|
-
- CPU model and core count
|
|
13
|
-
- Total RAM
|
|
14
|
-
- OS and kernel information
|
|
15
|
-
- Hardware Serial Number
|
|
16
|
-
- Hardware UUID
|
|
17
|
-
- Motherboard Serial Number
|
|
18
|
-
- Root disk volume UUID
|
|
19
|
-
- System Model Identifier
|
|
20
|
-
- GPU Information
|
|
21
|
-
- BIOS/Firmware Information
|
|
22
|
-
- Serial numbers of all physical disks
|
|
23
|
-
|
|
24
|
-
It then combines these components into a single string and uses the SHA-256 algorithm to produce a consistent, unique fingerprint.
|
|
25
|
-
|
|
26
|
-
## Use Cases
|
|
27
|
-
|
|
28
|
-
Here are some practical scenarios where `msid` can be valuable:
|
|
29
|
-
|
|
30
|
-
### Software Licensing
|
|
31
|
-
- **Offline License Activation**: Bind software licenses to specific machines without requiring constant internet validation.
|
|
32
|
-
- **Trial Period Management**: Track installations to prevent users from repeatedly installing trial versions.
|
|
33
|
-
- **License Auditing**: Keep track of which machines have your software installed for compliance purposes.
|
|
34
|
-
|
|
35
|
-
### Security Applications
|
|
36
|
-
- **Multi-factor Authentication**: Add an extra layer of security by validating not just "something you know" (password) but also "something you have" (the specific device).
|
|
37
|
-
- **Session Security**: Detect when a user's session moves to a different machine, potentially indicating session hijacking.
|
|
38
|
-
- **Fraud Detection**: Flag accounts that suddenly access your service from multiple different machines in rapid succession.
|
|
39
|
-
|
|
40
|
-
### System Management
|
|
41
|
-
- **Asset Tracking**: Inventory management for IT departments to track hardware across an organization.
|
|
42
|
-
- **Configuration Management**: Associate specific configurations or settings with particular machines.
|
|
43
|
-
- **Environment-specific Features**: Enable or disable features based on the hardware environment.
|
|
44
|
-
|
|
45
|
-
### DevOps and Deployment
|
|
46
|
-
- **Environment Fingerprinting**: Distinguish between development, staging, and production environments.
|
|
47
|
-
- **Deployment Validation**: Ensure that deployments only happen from authorized build machines.
|
|
48
|
-
- **Infrastructure Auditing**: Track which servers or virtual machines are running your applications.
|
|
49
|
-
|
|
50
|
-
### Usage Analytics
|
|
51
|
-
- **Device Demographics**: Gather anonymous statistics about what kinds of machines are running your software.
|
|
52
|
-
- **Installation Metrics**: Track unique installations versus reinstallations.
|
|
53
|
-
- **Hardware Compatibility**: Identify hardware configurations where your software performs well or poorly.
|
|
5
|
+
Useful for licensing, analytics, and security.
|
|
54
6
|
|
|
55
7
|
## Installation
|
|
56
8
|
|
|
57
|
-
Add this line to your application's Gemfile:
|
|
58
|
-
|
|
59
9
|
```ruby
|
|
60
10
|
gem 'msid'
|
|
61
11
|
```
|
|
62
12
|
|
|
63
|
-
And then execute:
|
|
64
|
-
|
|
65
|
-
$ bundle install
|
|
66
|
-
|
|
67
|
-
Or install it yourself as:
|
|
68
|
-
|
|
69
|
-
$ gem install msid
|
|
70
|
-
|
|
71
13
|
## Usage
|
|
72
14
|
|
|
73
|
-
### From Ruby
|
|
74
|
-
|
|
75
|
-
You can generate the machine ID from your Ruby code.
|
|
76
|
-
|
|
77
15
|
```ruby
|
|
78
16
|
require 'msid'
|
|
79
17
|
|
|
80
|
-
# Generate
|
|
81
|
-
|
|
82
|
-
puts machine_id
|
|
18
|
+
# Generate Machine ID
|
|
19
|
+
Msid.generate
|
|
83
20
|
# => "a1b2c3d4..."
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
#### Using a Salt
|
|
87
|
-
|
|
88
|
-
For enhanced security, you can provide a `salt`. This is useful if you want to generate different IDs for different applications on the same machine.
|
|
89
|
-
|
|
90
|
-
```ruby
|
|
91
|
-
require 'msid'
|
|
92
|
-
|
|
93
|
-
# Generate an ID with a salt
|
|
94
|
-
app_specific_id = Msid.generate(salt: 'my-super-secret-app-key')
|
|
95
|
-
puts app_specific_id
|
|
96
|
-
# => "e5f6g7h8..." (different from the one without a salt)
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### With IRB (for local development)
|
|
100
|
-
|
|
101
|
-
If you are developing the gem locally, you can test it with `irb`. From the root directory of the gem, run `irb` with the `-I` flag to add the `lib` directory to Ruby's load path.
|
|
102
21
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Then, inside `irb`, you can require and use the gem:
|
|
108
|
-
|
|
109
|
-
```ruby
|
|
110
|
-
irb(main):001:0> require 'msid'
|
|
111
|
-
=> true
|
|
112
|
-
irb(main):002:0> Msid.generate
|
|
113
|
-
=> "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### In a Rails Application
|
|
22
|
+
# With Salt (Recommended for App-Specific IDs)
|
|
23
|
+
Msid.generate(salt: 'my-secret-app-key')
|
|
117
24
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
```ruby
|
|
122
|
-
# Gemfile
|
|
123
|
-
gem 'msid', path: '/path/to/your/msid/gem'
|
|
25
|
+
# Inspect Gathered Data (New in v0.2.0)
|
|
26
|
+
Msid.info
|
|
27
|
+
# => {:hostname=>"MacBook-Pro.local", :cpu_model=>"Apple M1", ...}
|
|
124
28
|
```
|
|
125
29
|
|
|
126
|
-
|
|
127
|
-
```ruby
|
|
128
|
-
# Gemfile
|
|
129
|
-
gem 'msid'
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Then run `bundle install`.
|
|
133
|
-
|
|
134
|
-
Now you can use it anywhere in your Rails application, including the Rails console:
|
|
135
|
-
|
|
136
|
-
```sh
|
|
137
|
-
$ rails console
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
```ruby
|
|
141
|
-
# In Rails console
|
|
142
|
-
irb(main):001:0> Msid.generate
|
|
143
|
-
=> "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
|
|
144
|
-
|
|
145
|
-
# For extra security, you can use a Rails secret as a salt
|
|
146
|
-
irb(main):002:0> Msid.generate(salt: Rails.application.credentials.secret_key_base)
|
|
147
|
-
=> "f1e2d3c4b5a6f7e8d9c0b1a2f3e4d5c6b7a8f9e0d1c2b3a4f5e6d7c8b9a0f1e2"
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### From the Command Line
|
|
151
|
-
|
|
152
|
-
The gem provides a command-line executable to easily retrieve the machine ID.
|
|
30
|
+
### CLI
|
|
153
31
|
|
|
154
32
|
```sh
|
|
155
33
|
$ msid
|
|
156
|
-
|
|
34
|
+
a1b2c3d4...
|
|
157
35
|
```
|
|
158
36
|
|
|
159
|
-
## Development
|
|
160
|
-
|
|
161
|
-
After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests.
|
|
162
|
-
|
|
163
37
|
## Contributing
|
|
164
|
-
|
|
165
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/davidesantangelo/msid.
|
|
38
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/davidesantangelo/msid).
|
|
166
39
|
|
|
167
40
|
## License
|
|
168
|
-
|
|
169
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
41
|
+
MIT License.
|
data/lib/msid/source.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Msid
|
|
6
|
+
# Base class for platform-specific data sources.
|
|
7
|
+
# Subclasses should implement methods to gather system information.
|
|
8
|
+
class Source
|
|
9
|
+
# @return [Hash] A hash containing all gathered system information.
|
|
10
|
+
def gather
|
|
11
|
+
{
|
|
12
|
+
hostname: hostname,
|
|
13
|
+
mac_addresses: mac_addresses,
|
|
14
|
+
cpu_model: cpu_model,
|
|
15
|
+
cpu_cores: cpu_cores,
|
|
16
|
+
memory: memory,
|
|
17
|
+
os_kernel: os_kernel,
|
|
18
|
+
serial_number: serial_number,
|
|
19
|
+
hardware_uuid: hardware_uuid,
|
|
20
|
+
board_serial: board_serial,
|
|
21
|
+
disk_uuid: disk_uuid,
|
|
22
|
+
model_identifier: model_identifier,
|
|
23
|
+
gpu_model: gpu_model,
|
|
24
|
+
bios_version: bios_version,
|
|
25
|
+
disk_serials: disk_serials
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
protected
|
|
30
|
+
|
|
31
|
+
# Executes a shell command and returns its stripped stdout.
|
|
32
|
+
# Returns nil if the command fails or an error occurs.
|
|
33
|
+
def run(command)
|
|
34
|
+
stdout, _stderr, status = Open3.capture3(command)
|
|
35
|
+
status.success? ? stdout.strip : nil
|
|
36
|
+
rescue StandardError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Normalize a string/array for consistent ID generation
|
|
41
|
+
def normalize(value)
|
|
42
|
+
case value
|
|
43
|
+
when Array
|
|
44
|
+
value.compact.map { |v| normalize(v) }.reject(&:empty?).sort.join(',')
|
|
45
|
+
when String
|
|
46
|
+
value.strip
|
|
47
|
+
else
|
|
48
|
+
value
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Abstract methods to be implemented by subclasses
|
|
53
|
+
%i[hostname mac_addresses cpu_model cpu_cores memory os_kernel
|
|
54
|
+
serial_number hardware_uuid board_serial disk_uuid
|
|
55
|
+
model_identifier gpu_model bios_version disk_serials].each do |method|
|
|
56
|
+
define_method(method) { nil }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'msid/source'
|
|
4
|
+
require 'socket'
|
|
5
|
+
|
|
6
|
+
module Msid
|
|
7
|
+
module Sources
|
|
8
|
+
# MacOS specific data source implementation.
|
|
9
|
+
class Darwin < Source
|
|
10
|
+
def hostname
|
|
11
|
+
Socket.gethostname
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def mac_addresses
|
|
15
|
+
run("ifconfig -a | grep -o -E '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}'")&.split("\n")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def cpu_model
|
|
19
|
+
sysctl('machdep.cpu.brand_string')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def cpu_cores
|
|
23
|
+
sysctl('hw.ncpu')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def memory
|
|
27
|
+
sysctl('hw.memsize')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def os_kernel
|
|
31
|
+
"#{RUBY_PLATFORM}-#{run('uname -r')}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def serial_number
|
|
35
|
+
# system_profiler is slow, try ioreg first?
|
|
36
|
+
# For stability, sticking to system_profiler but ideally we'd cache this if called multiple times.
|
|
37
|
+
# However, for a one-off generation, a single call is fine.
|
|
38
|
+
# We can optimize by asking for *only* the specific data type.
|
|
39
|
+
run("system_profiler SPHardwareDataType | awk '/Serial Number/{print $4}'")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def hardware_uuid
|
|
43
|
+
run("system_profiler SPHardwareDataType | awk '/Hardware UUID/ {print $3}'")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def board_serial
|
|
47
|
+
# macOS doesn't easily expose this to user space without root or private entitlements sometimes.
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def disk_uuid
|
|
52
|
+
run("diskutil info / | awk '/Volume UUID/{print $3}'")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def model_identifier
|
|
56
|
+
sysctl('hw.model') # Faster than system_profiler
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def gpu_model
|
|
60
|
+
run("system_profiler SPDisplaysDataType | grep 'Chipset Model:' | awk -F': ' '{print $2}'")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def bios_version
|
|
64
|
+
run("system_profiler SPHardwareDataType | awk '/Boot ROM Version/ {print $4}'")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def disk_serials
|
|
68
|
+
run("system_profiler SPStorageDataType | grep 'Serial Number:' | awk '{print $3}'")&.split("\n")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def sysctl(key)
|
|
74
|
+
run("sysctl -n #{key}")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'msid/source'
|
|
4
|
+
require 'socket'
|
|
5
|
+
|
|
6
|
+
module Msid
|
|
7
|
+
module Sources
|
|
8
|
+
# Linux specific data source implementation.
|
|
9
|
+
class Linux < Source
|
|
10
|
+
def hostname
|
|
11
|
+
Socket.gethostname
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def mac_addresses
|
|
15
|
+
# More reliable than parsing `ip` command output
|
|
16
|
+
Dir.glob('/sys/class/net/*/address').map { |f| File.read(f).strip }
|
|
17
|
+
rescue StandardError
|
|
18
|
+
[]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def cpu_model
|
|
22
|
+
run("grep 'model name' /proc/cpuinfo | uniq | awk -F': ' '{print $2}'")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def cpu_cores
|
|
26
|
+
run('nproc')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def memory
|
|
30
|
+
run("grep MemTotal /proc/meminfo | awk '{print $2}'")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def os_kernel
|
|
34
|
+
"#{RUBY_PLATFORM}-#{run('uname -r')}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def serial_number
|
|
38
|
+
run('cat /sys/class/dmi/id/product_serial')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def hardware_uuid
|
|
42
|
+
run('cat /sys/class/dmi/id/product_uuid')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def board_serial
|
|
46
|
+
run('cat /sys/class/dmi/id/board_serial')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def disk_uuid
|
|
50
|
+
device = run("df / | tail -n1 | awk '{print $1}'")
|
|
51
|
+
run("lsblk -no UUID #{device}") if device
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def model_identifier
|
|
55
|
+
run('cat /sys/class/dmi/id/product_name')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def gpu_model
|
|
59
|
+
run("lspci | grep -i 'vga\\|3d\\|2d' | head -n 1 | awk -F': ' '{print $3}'")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def bios_version
|
|
63
|
+
vendor = run('cat /sys/class/dmi/id/bios_vendor')
|
|
64
|
+
version = run('cat /sys/class/dmi/id/bios_version')
|
|
65
|
+
"#{vendor}-#{version}" if vendor && version
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def disk_serials
|
|
69
|
+
run('lsblk -d -o serial -n')&.split("\n")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'msid/source'
|
|
4
|
+
require 'socket'
|
|
5
|
+
|
|
6
|
+
module Msid
|
|
7
|
+
module Sources
|
|
8
|
+
# Windows specific data source implementation.
|
|
9
|
+
class Windows < Source
|
|
10
|
+
def hostname
|
|
11
|
+
Socket.gethostname
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def mac_addresses
|
|
15
|
+
run('getmac /v /fo csv | findstr /V "Disconnected"')
|
|
16
|
+
&.lines
|
|
17
|
+
&.map { |line| line.split(',')[2]&.gsub('"', '')&.strip }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def cpu_model
|
|
21
|
+
wmic('cpu get name')&.gsub('Name=', '')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def cpu_cores
|
|
25
|
+
wmic('cpu get NumberOfCores')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def memory
|
|
29
|
+
wmic('ComputerSystem get TotalPhysicalMemory')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def os_kernel
|
|
33
|
+
"#{RUBY_PLATFORM}-#{run('ver')}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def serial_number
|
|
37
|
+
wmic('bios get serialnumber')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def hardware_uuid
|
|
41
|
+
wmic('csproduct get uuid')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def board_serial
|
|
45
|
+
wmic('baseboard get serialnumber')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def disk_uuid
|
|
49
|
+
run('vol c: | findstr "Serial Number"')&.split&.last
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def model_identifier
|
|
53
|
+
wmic('csproduct get name')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def gpu_model
|
|
57
|
+
wmic('path win32_videocontroller get name')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def bios_version
|
|
61
|
+
run('wmic bios get manufacturer,version /format:list')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def disk_serials
|
|
65
|
+
wmic('diskdrive get serialnumber')&.split("\n")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Helper to run wmic commands and strip headers
|
|
71
|
+
def wmic(query)
|
|
72
|
+
# WMIC output often contains the header repeated or extra whitespace.
|
|
73
|
+
# This is a basic filter based on the original implementation's logic (using findstr /V).
|
|
74
|
+
# We'll rely on the specific parsing logic from original which used findstr /V "HeaderName"
|
|
75
|
+
# Since we want to be generic here, let's keep it simple:
|
|
76
|
+
# Original: run('wmic cpu get NumberOfCores | findstr /V "NumberOfCores"')
|
|
77
|
+
# So we just construct that command.
|
|
78
|
+
|
|
79
|
+
# Extract the property name from the query "cpu get NumberOfCores" -> "NumberOfCores"
|
|
80
|
+
# This is valid for simple "get X" queries.
|
|
81
|
+
property = query.split.last
|
|
82
|
+
run("wmic #{query} | findstr /V \"#{property}\"")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/msid/version.rb
CHANGED
data/lib/msid.rb
CHANGED
|
@@ -1,248 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'msid/version'
|
|
4
|
-
require '
|
|
4
|
+
require 'msid/source'
|
|
5
5
|
require 'digest'
|
|
6
|
-
require 'open3'
|
|
7
6
|
|
|
8
7
|
module Msid
|
|
9
8
|
class Error < StandardError; end
|
|
10
9
|
|
|
11
|
-
# Generates a unique machine fingerprint ID.
|
|
12
|
-
#
|
|
13
|
-
# The fingerprint is created by collecting various system identifiers,
|
|
14
|
-
# combining them into a single string, and then hashing it with SHA-256.
|
|
15
|
-
# This makes the ID highly unique to the machine it's generated on.
|
|
16
10
|
class Generator
|
|
17
11
|
class << self
|
|
18
|
-
# Gathers a collection of machine-specific identifiers.
|
|
19
|
-
# @return [Array<String>] a list of identifiers.
|
|
20
|
-
def gather_components
|
|
21
|
-
[
|
|
22
|
-
hostname,
|
|
23
|
-
all_mac_addresses,
|
|
24
|
-
cpu_info,
|
|
25
|
-
cpu_cores,
|
|
26
|
-
total_memory,
|
|
27
|
-
os_info,
|
|
28
|
-
serial_number,
|
|
29
|
-
hardware_uuid,
|
|
30
|
-
baseboard_serial,
|
|
31
|
-
disk_uuid,
|
|
32
|
-
system_model,
|
|
33
|
-
gpu_info,
|
|
34
|
-
bios_info,
|
|
35
|
-
all_disk_serials
|
|
36
|
-
].compact.reject(&:empty?)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
12
|
# Generates the unique machine ID.
|
|
40
13
|
# @param salt [String, nil] An optional salt to add to the fingerprint.
|
|
41
14
|
# @return [String] The SHA-256 hash representing the machine ID.
|
|
42
15
|
def generate(salt: nil)
|
|
43
16
|
components = gather_components
|
|
44
|
-
raise Error, 'Could not gather any machine identifiers.' if components.empty?
|
|
17
|
+
raise Error, 'Could not gather any machine identifiers.' if components.values.reject { |v| v.nil? || v.empty? }.empty?
|
|
45
18
|
|
|
46
|
-
components.
|
|
19
|
+
data_string = components.values.compact.reject(&:empty?).join(':')
|
|
20
|
+
data_string += ":#{salt}" if salt
|
|
47
21
|
|
|
48
|
-
data_string = components.join(':')
|
|
49
22
|
Digest::SHA256.hexdigest(data_string)
|
|
50
23
|
end
|
|
51
24
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def run_command(command)
|
|
57
|
-
stdout, _stderr, status = Open3.capture3(command)
|
|
58
|
-
status.success? ? stdout.strip : nil
|
|
59
|
-
rescue StandardError
|
|
60
|
-
nil
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# @return [String, nil] The machine's hostname.
|
|
64
|
-
def hostname
|
|
65
|
-
Socket.gethostname
|
|
66
|
-
rescue StandardError
|
|
67
|
-
nil
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# @return [String, nil] A sorted, concatenated string of all MAC addresses.
|
|
71
|
-
def all_mac_addresses
|
|
72
|
-
macs = case RUBY_PLATFORM
|
|
73
|
-
when /darwin/ # macOS
|
|
74
|
-
run_command("ifconfig -a | grep -o -E '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}'")&.split("\n")
|
|
75
|
-
when /linux/
|
|
76
|
-
# Read from /sys/class/net, which is more reliable than parsing `ip` command output
|
|
77
|
-
begin
|
|
78
|
-
Dir.glob('/sys/class/net/*/address').map { |f| File.read(f).strip }
|
|
79
|
-
rescue StandardError
|
|
80
|
-
[]
|
|
81
|
-
end
|
|
82
|
-
when /mswin|mingw/ # Windows
|
|
83
|
-
run_command('getmac /v /fo csv | findstr /V "Disconnected"')
|
|
84
|
-
&.lines
|
|
85
|
-
&.map { |line| line.split(',')[2]&.gsub('"', '')&.strip }
|
|
86
|
-
else
|
|
87
|
-
[]
|
|
88
|
-
end
|
|
89
|
-
macs&.compact&.reject(&:empty?)&.sort&.join(',')
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# @return [String, nil] CPU model information.
|
|
93
|
-
def cpu_info
|
|
94
|
-
case RUBY_PLATFORM
|
|
95
|
-
when /darwin/
|
|
96
|
-
run_command('sysctl -n machdep.cpu.brand_string')
|
|
97
|
-
when /linux/
|
|
98
|
-
run_command("grep 'model name' /proc/cpuinfo | uniq | awk -F': ' '{print $2}'")
|
|
99
|
-
when /mswin|mingw/
|
|
100
|
-
run_command('wmic cpu get name /format:list | findstr "Name="')&.gsub('Name=', '')
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# @return [String, nil] Number of CPU cores.
|
|
105
|
-
def cpu_cores
|
|
106
|
-
case RUBY_PLATFORM
|
|
107
|
-
when /darwin/
|
|
108
|
-
run_command('sysctl -n hw.ncpu')
|
|
109
|
-
when /linux/
|
|
110
|
-
run_command('nproc')
|
|
111
|
-
when /mswin|mingw/
|
|
112
|
-
run_command('wmic cpu get NumberOfCores | findstr /V "NumberOfCores"')
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# @return [String, nil] Total physical memory in bytes or KB.
|
|
117
|
-
def total_memory
|
|
118
|
-
case RUBY_PLATFORM
|
|
119
|
-
when /darwin/
|
|
120
|
-
run_command('sysctl -n hw.memsize')
|
|
121
|
-
when /linux/
|
|
122
|
-
run_command("grep MemTotal /proc/meminfo | awk '{print $2}'")
|
|
123
|
-
when /mswin|mingw/
|
|
124
|
-
run_command('wmic ComputerSystem get TotalPhysicalMemory | findstr /V "TotalPhysicalMemory"')
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# @return [String, nil] Operating system and kernel information.
|
|
129
|
-
def os_info
|
|
130
|
-
kernel_info = case RUBY_PLATFORM
|
|
131
|
-
when /mswin|mingw/
|
|
132
|
-
run_command('ver')
|
|
133
|
-
else
|
|
134
|
-
run_command('uname -r')
|
|
135
|
-
end
|
|
136
|
-
"#{RUBY_PLATFORM}-#{kernel_info}"
|
|
25
|
+
# Returns a hash of all gathered components.
|
|
26
|
+
# @return [Hash]
|
|
27
|
+
def info
|
|
28
|
+
gather_components
|
|
137
29
|
end
|
|
138
30
|
|
|
139
|
-
|
|
140
|
-
def serial_number
|
|
141
|
-
case RUBY_PLATFORM
|
|
142
|
-
when /darwin/
|
|
143
|
-
run_command("system_profiler SPHardwareDataType | awk '/Serial Number/{print $4}'")
|
|
144
|
-
when /linux/
|
|
145
|
-
run_command('cat /sys/class/dmi/id/product_serial')
|
|
146
|
-
when /mswin|mingw/
|
|
147
|
-
run_command('wmic bios get serialnumber | findstr /V "SerialNumber"')
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# @return [String, nil] The machine's hardware UUID.
|
|
152
|
-
def hardware_uuid
|
|
153
|
-
case RUBY_PLATFORM
|
|
154
|
-
when /darwin/
|
|
155
|
-
run_command("system_profiler SPHardwareDataType | awk '/Hardware UUID/ {print $3}'")
|
|
156
|
-
when /linux/
|
|
157
|
-
run_command('cat /sys/class/dmi/id/product_uuid')
|
|
158
|
-
when /mswin|mingw/
|
|
159
|
-
run_command('wmic csproduct get uuid | findstr /V "UUID"')
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
# @return [String, nil] The machine's baseboard serial number.
|
|
164
|
-
def baseboard_serial
|
|
165
|
-
case RUBY_PLATFORM
|
|
166
|
-
when /darwin/
|
|
167
|
-
nil # Not easily available/distinct from system serial on macOS
|
|
168
|
-
when /linux/
|
|
169
|
-
run_command('cat /sys/class/dmi/id/board_serial')
|
|
170
|
-
when /mswin|mingw/
|
|
171
|
-
run_command('wmic baseboard get serialnumber | findstr /V "SerialNumber"')
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# @return [String, nil] The UUID of the root volume.
|
|
176
|
-
def disk_uuid
|
|
177
|
-
case RUBY_PLATFORM
|
|
178
|
-
when /darwin/
|
|
179
|
-
run_command("diskutil info / | awk '/Volume UUID/{print $3}'")
|
|
180
|
-
when /linux/
|
|
181
|
-
device = run_command("df / | tail -n1 | awk '{print $1}'")
|
|
182
|
-
run_command("lsblk -no UUID #{device}") if device
|
|
183
|
-
when /mswin|mingw/
|
|
184
|
-
run_command('vol c: | findstr "Serial Number"')&.split&.last
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# @return [String, nil] The machine's system model identifier.
|
|
189
|
-
def system_model
|
|
190
|
-
case RUBY_PLATFORM
|
|
191
|
-
when /darwin/
|
|
192
|
-
run_command("system_profiler SPHardwareDataType | awk '/Model Identifier/ {print $3}'")
|
|
193
|
-
when /linux/
|
|
194
|
-
run_command('cat /sys/class/dmi/id/product_name')
|
|
195
|
-
when /mswin|mingw/
|
|
196
|
-
run_command('wmic csproduct get name | findstr /V "Name"')
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# @return [String, nil] The machine's GPU information.
|
|
201
|
-
def gpu_info
|
|
202
|
-
case RUBY_PLATFORM
|
|
203
|
-
when /darwin/
|
|
204
|
-
run_command("system_profiler SPDisplaysDataType | grep 'Chipset Model:' | awk -F': ' '{print $2}'")
|
|
205
|
-
when /linux/
|
|
206
|
-
run_command("lspci | grep -i 'vga\\|3d\\|2d' | head -n 1 | awk -F': ' '{print $3}'")
|
|
207
|
-
when /mswin|mingw/
|
|
208
|
-
run_command('wmic path win32_videocontroller get name | findstr /V "Name"')
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
# @return [String, nil] The machine's BIOS/firmware information.
|
|
213
|
-
def bios_info
|
|
214
|
-
case RUBY_PLATFORM
|
|
215
|
-
when /darwin/
|
|
216
|
-
run_command("system_profiler SPHardwareDataType | awk '/Boot ROM Version/ {print $4}'")
|
|
217
|
-
when /linux/
|
|
218
|
-
vendor = run_command('cat /sys/class/dmi/id/bios_vendor')
|
|
219
|
-
version = run_command('cat /sys/class/dmi/id/bios_version')
|
|
220
|
-
"#{vendor}-#{version}" if vendor && version
|
|
221
|
-
when /mswin|mingw/
|
|
222
|
-
run_command('wmic bios get manufacturer,version /format:list')
|
|
223
|
-
end
|
|
224
|
-
end
|
|
31
|
+
private
|
|
225
32
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
33
|
+
def gather_components
|
|
34
|
+
@components ||= source.gather
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def source
|
|
38
|
+
@source ||= case RUBY_PLATFORM
|
|
39
|
+
when /darwin/
|
|
40
|
+
require 'msid/sources/darwin'
|
|
41
|
+
Msid::Sources::Darwin.new
|
|
42
|
+
when /linux/
|
|
43
|
+
require 'msid/sources/linux'
|
|
44
|
+
Msid::Sources::Linux.new
|
|
45
|
+
when /mswin|mingw/
|
|
46
|
+
require 'msid/sources/windows'
|
|
47
|
+
Msid::Sources::Windows.new
|
|
48
|
+
else
|
|
49
|
+
raise Error, "Unsupported platform: #{RUBY_PLATFORM}"
|
|
50
|
+
end
|
|
239
51
|
end
|
|
240
52
|
end
|
|
241
53
|
end
|
|
242
54
|
|
|
243
55
|
# Shortcut for Msid::Generator.generate
|
|
244
|
-
# @param salt [String, nil] An optional salt to add to the fingerprint.
|
|
245
56
|
def self.generate(salt: nil)
|
|
246
57
|
Generator.generate(salt: salt)
|
|
247
58
|
end
|
|
59
|
+
|
|
60
|
+
# Shortcut for Msid::Generator.info
|
|
61
|
+
def self.info
|
|
62
|
+
Generator.info
|
|
63
|
+
end
|
|
248
64
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: msid
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Davide Santangelo
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-12-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Creates a unique and secure machine fingerprint ID by gathering various
|
|
14
14
|
system hardware and software identifiers. Designed to be difficult to replicate
|
|
@@ -25,6 +25,10 @@ files:
|
|
|
25
25
|
- README.md
|
|
26
26
|
- Rakefile
|
|
27
27
|
- lib/msid.rb
|
|
28
|
+
- lib/msid/source.rb
|
|
29
|
+
- lib/msid/sources/darwin.rb
|
|
30
|
+
- lib/msid/sources/linux.rb
|
|
31
|
+
- lib/msid/sources/windows.rb
|
|
28
32
|
- lib/msid/version.rb
|
|
29
33
|
- msid.gemspec
|
|
30
34
|
homepage: https://github.com/davidesantangelo/msid
|