description = [[
Attempts to discover Microsoft SQL Server instances and extract configuration
and version information.

NOTE: Unlike previous versions, this script will NOT attempt to log in to SQL
Server instances. Blank passwords can be checked using the
<code>ms-sql-empty-password</code> script.

The primary purpose of the script is to query the SQL Server Monitor/SQL Server
Browser service, which runs on UDP port 1434 (for more information, see:
http://msdn.microsoft.com/en-us/library/ms181087.aspx), to retrieve the list of
SQL Server instances installed on the host. The information for each instance
includes the instance name, the version (which may not be accurate), and
possibly networking information for the instance, such as a TCP port.

The script will then attempt to query each SQL Server instance accessible via
TCP in order to obtain an accurate version number. Using this version number,
the script will determine what service pack (if any) the instance has installed
and whether any additional patches have been installed on top of the service
pack.

The script can be prevented from connecting to each instance via TCP by using
the <code>ms-sql-info.browseronly</code> script argument. However, this will
cause the script to rely only upon the version number provided by the SQL Server
Browser/Monitor, which has the following limitations:
* For SQL Server 2000 and SQL Server 7.0 instances, the RTM version number is
always given, regardless of any service packs or patches installed.
* For SQL Server 2005 and later, the version number will reflect the service
pack installed, but the script will not be able to tell whether patches have
been installed.

Where possible, the script will determine major version numbers, service pack
levels and whether patches have been installed. However, in cases where
particular determinations can not be made (because the script was unable to
connect to an instance via TCP, or because the <code>ms-sql-info.browseronly</code>
argument was set), the script will report only what can be confirmed.  

The script will also run against any identified TCP listeners for SQL Server
instances. In these cases, the script will only attempt to obtain the version
number and determine the product version, service pack level and patch status.
]]
---
-- @usage
-- nmap -sS -p 1433 --script ms-sql-info <host>
-- nmap -sU -p 1434 --script ms-sql-info <host>
--
-- @args ms-sql-info.browseronly If set, the script will not connect to
--			individual SQL Server instances (via TCP) to get version
--			information. The script will only get version information from the
--			SQL Server Browser service (UDP 1434), which is less accurate.
--			However, this will prevent the script from connecting to ports
--			that weren't part of the Nmap scan.
--
-- @output
-- PORT     STATE SERVICE
-- 1434/udp open  ms-sql-m
-- | ms-sql-info:
-- |   Server name: WINXP
-- |     Instance name: SQL2K5
-- |       Version: Microsoft SQL Server 2005 SP3+
-- |         Version number: 9.00.4053
-- |         Product: Microsoft SQL Server 2005
-- |         Service pack level: SP3
-- |         Post-SP patches applied: Yes
-- |       TCP port: 1278
-- |       Clustered: No
-- |     Instance name: SQLFIREWALLED
-- |       Version: Microsoft SQL Server 2008 RTM
-- |         Product: Microsoft SQL Server 2008
-- |         Service pack level: RTM
-- |       TCP port: 4343
-- |       Clustered: No
-- |     Instance name: MSSQLSERVER
-- |       Version: Microsoft SQL Server 2000 SP4
-- |         Version number: 8.00.2039
-- |         Product: Microsoft SQL Server 2000
-- |         Service pack level: SP4
-- |         Post-SP patches applied: No
-- |       TCP port: 1433
-- |       Named pipe: \\WINXP\pipe\sql\query
-- |_      Clustered: No
-- 1433/tcp open  ms-sql-s
-- | ms-sql-info:
-- |     Instance name: MSSQLSERVER
-- |       Version: Microsoft SQL Server 2000 SP4
-- |         Version number: 8.00.2039
-- |         Product: Microsoft SQL Server 2000
-- |         Service pack level: SP4
-- |         Post-SP patches applied: No
-- |_      TCP port: 1433
--

-- rev 1.0 (2007-06-09)
-- rev 1.1 (2009-12-06 - Added SQL 2008 identification T Sellers)
-- rev 1.2 (2010-10-03 - Added Broadcast support <patrik@cqure.net>)
-- rev 1.3 (2010-10-10 - Added prerule and newtargets support <patrik@cqure.net>)
-- rev 1.4 (2011-01-24 - Revised logic in order to get version data without logging in;
--                       added functionality to interpret version in terms of SP level, etc.
--                       added script arg to prevent script from connecting to ports that
--                         weren't in original Nmap scan <chris3E3@gmail.com>)

author = "Chris Woodbury, Thomas Buchanan"

license = "Same as Nmap--See http://nmap.org/book/man-legal.html"

categories = {"default", "discovery", "safe"}

require("bin")
require("shortport")
require("mssql")
require("strbuf")
require("nsedebug")

prerule = function() return false end
portrule = function(host, port)
	local isSqlBrowser = shortport.portnumber( 1434, "udp" )
	local isSqlServer = shortport.port_or_service( 1433, "ms-sql-s" ) 

	return isSqlBrowser or isSqlServer
end

	
-- TDS packet types
PacketType =
{
	Query = 0x01,
	Response = 0x04,
	Login = 0x10,
	PreLogin = 0x12,
}

-- TDS pre-login option types
PreLoginOptionType = 
{
	Version = 0x00,
	Encryption = 0x01,
	InstOpt = 0x02,
	ThreadId = 0x03,
	MARS = 0x04,
	Terminator = 0xFF,
}

-- Lengths for the values of TDS pre-login option fields
PreLoginOptionLength_Client = 
{
	[PreLoginOptionType.Version] = 6,
	[PreLoginOptionType.Encryption] = 1,
	[PreLoginOptionType.InstOpt] = -1,
	[PreLoginOptionType.ThreadId] = 4,
	[PreLoginOptionType.MARS] = 1,
	[PreLoginOptionType.Terminator] = 0,
}

-- Lengths for the values of TDS pre-login option fields
PreLoginOptionLength_Server = 
{
	[PreLoginOptionType.Version] = 6,
	[PreLoginOptionType.Encryption] = 1,
	[PreLoginOptionType.InstOpt] = -1,
	[PreLoginOptionType.ThreadId] = -1, -- According to the TDS spec, this value "should" be empty in the server's response
	[PreLoginOptionType.MARS] = 1,
	[PreLoginOptionType.Terminator] = 0,
}

-- Helper class (would be merged into mssql.Helper)
Helper = 
{	--- Attempts to connect to a SQL Server instance listening on a TCP port in
	--  order to determine the version of the SSNetLib DLL, which is an
	--  authoritative version number for the SQL Server instance itself.
	--
	-- @param host the hostname or IP address of the target instance (required)
	-- @param tcpPort the TCP port on which the instance is listening (required)
	-- @param instanceName the name of the SQL Server instance (optional, but
	--			omitting it may result in failure to get the version info)
	-- @return status true on success, false on failure
	-- @return versionInfo an instance of mssql.SqlServerVersionInfo, or nil
	GetInstanceVersion = function( host, tcpPort, instanceName )
			
		local status, response, version
		local tdsStream = mssql.TDSStream:new()
		
		status, response = tdsStream:Connect( host, tcpPort )
		if ( not(status) ) then
			stdnse.print_debug( 2, "%s: Connection to %s:%s failed: %s",
				"mssql.lua", host or "nil", tcpPort or "nil", response or "" )
			return false, "Connect failed"
		end
		
		local preLoginRequest = PreLoginPacket:new()
		preLoginRequest:SetInstanceName( instanceName )
		
		tdsStream:SetTimeout( 5000 )
		tdsStream:Send( preLoginRequest:ToBytes() )
		
		-- read in any response we might get
		status, response = tdsStream:Receive()
		tdsStream:Disconnect()
		
		if status then
			local preLoginResponse
			status, preLoginResponse = PreLoginPacket.FromBytes( response )
			if status then
				version = preLoginResponse.versionInfo
			else
				stdnse.print_debug( 2, "%s: Parsing of pre-login packet from %s:%s failed: %s",
					"mssql.lua", host or "nil", tcpPort or "nil", preLoginResponse or "" )
				return false, "Parsing failed"
			end
		else
			stdnse.print_debug( 2, "%s: Receive for %s:%s failed: %s",
				"mssql.lua", host or "nil", tcpPort or "nil", response or "" )
			return false, "Receive failed"
		end
		
		return status, version
	end,
}


--- PreLoginPacket class
PreLoginPacket = 
{
	versionInfo = nil,
	__requestEncryption = 0,
	__instanceName = "",
	__threadId = 0, -- Dummy value; will be filled in later
	__requestMars = nil,

	new = function(self,o)
		o = o or {}
        setmetatable(o, self)
        self.__index = self
		return o
    end,
	
	--- Sets the client version (default = 9.00.00.00)
	--
	-- @param versionInfo version table with the client version information
	SetVersion = function(self, versionInfo)
		self.__versionInfo = versionInfo
	end,
	
	--- Sets whether to request encryption (default = false)
	--
	-- @param requestEncryption boolean indicating whether encryption will be requested
	SetRequestEncryption = function(self, requestEncryption)
		if requestEncryption then
			self.__requestEncryption = 1
		else
			self.__requestEncryption = 0
		end
	end,
	
	--- Sets whether to request MARS support (default = undefined)
	--
	-- @param requestMars boolean indicating whether MARS support will be requested
	SetRequestMars = function(self, requestMars)
		if requestMars then
			self.__requestMars = 1
		else
			self.__requestMars = 0
		end
	end,

	--- Sets the instance name of the target
	--
	-- @param instanceName string containing the name of the instance	
	SetInstanceName = function(self, instanceName)
		self.__instanceName = instanceName or ""
	end,
	
	--- Returns the pre-login packet as a byte string
	--
	-- @return byte string containing the pre-login packet
	ToBytes = function(self)
		local data, optionLength
		local offset = 1 -- Terminator
		offset = offset + 5 -- Version
		offset = offset + 5 -- Encryption
		offset = offset + 5 -- InstOpt
		offset = offset + 5 -- ThreadId
		if self.__requestMars then offset = offset + 3 end -- MARS
		
		math.randomseed(os.time())
		self.__threadId = math.random(1000)
		
		if not self.versionInfo then
			self.versionInfo = SqlServerVersionInfo:new()
			self.versionInfo:SetVersionNumber( "9.00.1399.00" )
		end
		
		optionLength = PreLoginOptionLength_Client[ PreLoginOptionType.Version ]
		data = bin.pack( ">CSS", PreLoginOptionType.Version, offset, optionLength )
		offset = offset + optionLength
		
		optionLength = PreLoginOptionLength_Client[ PreLoginOptionType.Encryption ]
		data = data .. bin.pack( ">CSS", PreLoginOptionType.Encryption, offset, optionLength )
		offset = offset + optionLength
		
		optionLength = #self.__instanceName + 1 --(string length + null-terminator)
		data = data .. bin.pack( ">CSS", PreLoginOptionType.InstOpt, offset, optionLength )
		offset = offset + optionLength
		
		optionLength = PreLoginOptionLength_Client[ PreLoginOptionType.ThreadId ]
		data = data .. bin.pack( ">CSS", PreLoginOptionType.ThreadId, offset, optionLength )
		offset = offset + optionLength
		
		if self.requestMars then
			optionLength = PreLoginOptionLength_Client[ PreLoginOptionType.MARS ]
			data = data .. bin.pack( ">CSS", PreLoginOptionType.MARS, offset, optionLength )
			offset = offset + optionLength
		end
		
		data = data .. bin.pack( "C", PreLoginOptionType.Terminator )
		
		-- Now that the pre-login headers are done, write the data
		data = data .. bin.pack( ">CCSS", self.versionInfo.major, self.versionInfo.minor,
					self.versionInfo.build, self.versionInfo.subBuild )
		data = data .. bin.pack( "C", self.__requestEncryption )
		data = data .. bin.pack( "z", self.__instanceName )
		data = data .. bin.pack( "<I", self.__threadId )
		if self.requestMars then
			data = data .. bin.pack( "C", self.__requestMars )
		end
		
		return PacketType.PreLogin, data
	end,
	
	--- 
	FromBytes = function( bytes )
		local status, pos = false, 1
		local preLoginPacket = PreLoginPacket:new()
		
		while true do
		
			local optionType, optionPos, optionLength, optionData, expectedOptionLength, _
			pos, optionType = bin.unpack("C", bytes, pos)
			if ( optionType == PreLoginOptionType.Terminator ) then
				status = true
				break
			end
			expectedOptionLength = PreLoginOptionLength_Server[ optionType ]
			if ( not expectedOptionLength ) then
				stdnse.print_debug( 2, "%s: Unrecognized pre-login option type: %s", "mssql.lua", optionType )
				expectedOptionLength = -1
			end
			
			pos, optionPos, optionLength = bin.unpack(">SS", bytes, pos)
			if not (optionPos and optionLength) then
				stdnse.print_debug( 2, "%s: Could not unpack optionPos and optionLength.", "mssql.lua" )
				return false, "Invalid pre-login response"
			end
			
			optionPos = optionPos + 1 -- convert from 0-based index to 1-based index
			if ( (optionPos + optionLength) > (#bytes + 1) ) then
				stdnse.print_debug( 2, "%s: Pre-login response: pos+len for option type %s is beyond end of data.", "mssql.lua", optionType )
				stdnse.print_debug( 2, "%s:   (optionPos: %s) (optionLength: %s)", "mssql.lua", optionPos, optionLength )
				return false, "Invalid pre-login response"
			end
			
			
			if ( optionLength ~= expectedOptionLength and expectedOptionLength ~= -1 ) then
				stdnse.print_debug( 2, "%s: Option data is incorrect size in pre-login response. ", "mssql.lua" )
				stdnse.print_debug( 2, "%s:   (optionType: %s) (optionLength: %s)", "mssql.lua", optionType, optionLength )
				return false, "Invalid pre-login response"
			end
			optionData = string.sub( bytes, optionPos, optionPos + optionLength - 1 )
			if #optionData ~= optionLength then
				stdnse.print_debug( 2, "%s: Could not read sufficient bytes from version data.", "mssql.lua" )
				return false, "Invalid pre-login response"
			end

			if ( optionType == PreLoginOptionType.Version ) then
				local major, minor, build, subBuild, version
				major = string.byte( optionData:sub( 1, 1 ) )
				minor = string.byte( optionData:sub( 2, 2 ) )
				build = (string.byte( optionData:sub( 3, 3 ) ) * 256) + string.byte( optionData:sub( 4, 4 ) )
				subBuild = (string.byte( optionData:sub( 5, 5 ) ) * 256) + string.byte( optionData:sub( 6, 6 ) )
				
				version = SqlServerVersionInfo:new()
				version:SetVersion( major, minor, build, subBuild, "SSNetLib" )
				preLoginPacket.versionInfo = version
			elseif ( optionType == PreLoginOptionType.Encryption ) then
				preLoginPacket:SetRequestEncryption( bin.unpack( "C", optionData ) )
			elseif ( optionType == PreLoginOptionType.InstOpt ) then
				preLoginPacket:SetInstanceName( bin.unpack( "z", optionData ) )
			elseif ( optionType == PreLoginOptionType.ThreadId ) then
				-- Do nothing. According to the TDS spec, this option is empty when sent from the server
			elseif ( optionType == PreLoginOptionType.MARS ) then
				preLoginPacket:SetRequestMars( bin.unpack( "C", optionData ) )
			end
		end
		
		return status, preLoginPacket
	end,
}


--- SqlServerVersionInfo class
SqlServerVersionInfo = 
{
	versionNumber = "",		-- The full version string (e.g. "9.00.2047.00") 
	major = nil,			-- The major version (e.g. 9)
	minor = nil,			-- The minor version (e.g. 0)
	build = nil,			-- The build number (e.g. 2047)
	subBuild = nil,			-- The sub-build number (e.g. 0)
	productName = nil,		-- The prodcut name (e.g. "SQL Server 2005")
	brandedVersion = nil,	-- The branded version of the product (e.g. "2005")
	servicePackLevel = nil,	-- The service pack leve (e.g. "SP1")
	patched = nil,			-- Whether patches have been applied since SP installation (true/false/nil)
	source = nil,			-- The source of the version info (e.g. "SSRP", "SSNetLib")

	new = function(self,o)
		o = o or {}
        setmetatable(o, self)
        self.__index = self
		return o
    end,
	
	--- Sets the version using a version number string.
	--
	-- @param versionNumber a version number string (e.g. "9.00.1399.00")
	-- @param source a string indicating the source of the version info (e.g. "SSRP", "SSNetLib")
	SetVersionNumber = function(self, versionNumber, source)
		local major, minor, revision, subBuild
		if versionNumber:match( "^%d+%.%d+%.%d+.%d+" ) then
			major, minor, revision, subBuild = versionNumber:match( "^(%d+)%.(%d+)%.(%d+)" )
		elseif versionNumber:match( "^%d+%.%d+%.%d+" ) then
			major, minor, revision = versionNumber:match( "^(%d+)%.(%d+)%.(%d+)" )
		else
			stdnse.print_debug( 1, "%s: SetVersionNumber: versionNumber is not in correct format: %s", "mssql.lua", versionNumber or "nil" )
		end

		self:SetVersion( major, minor, revision, subBuild, source )
	end,
	
	--- Sets the version using the individual numeric components of the version
	--  number.
	--
	-- @param source a string indicating the source of the version info (e.g. "SSRP", "SSNetLib")
	SetVersion = function(self, major, minor, build, subBuild, source)
		self.source = source
		-- make sure our version numbers all end up as valid numbers
		self.major, self.minor, self.build, self.subBuild = 
			tonumber( major or 0 ), tonumber( minor or 0 ), tonumber( build or 0 ), tonumber( subBuild or 0 )
		
		self.versionNumber = string.format( "%u.%02u.%u.%02u", self.major, self.minor, self.build, self.subBuild )
		
		self:__ParseVersionInfo()
	end,
		
	--- Using the version number, determines the product version
	__InferProductVersion = function(self)
		
		local VERSION_LOOKUP_TABLE = {
			["^6%.0"] = "6.0", ["^6%.5"] = "6.5", ["^7%.0"] = "7.0", 
			["^8%.0"] = "2000",	["^9%.0"] = "2005",	["^10%.0"] = "2008",
			["^10%.50"] = "2008 R2", ["^11%.0"] = "2011",
		}
		
		local product = ""
	
		for m, v in pairs(VERSION_LOOKUP_TABLE) do
			if ( self.versionNumber:match(m) ) then
				product = v
				self.brandedVersion = product
				break
			end
		end
		
		self.productName = ("Microsoft SQL Server %s"):format(product)
		
	end,
	
	
	--- Returns a lookup table that maps revision numbers to service pack levels for
	--  the applicable SQL Server version (e.g. { {1600, "RTM"}, {2531, "SP1"} }).
	__GetSpLookupTable = function(self)
	
		-- Service pack lookup tables: 
		-- For instances where a revised service pack was released (e.g. 2000 SP3a), we will include the
		-- build number for the original SP and the build number for the revision. However, leaving it
		-- like this would make it appear that subsequent builds were a patched version of the revision
		-- (e.g. a patch applied to 2000 SP3 that increased the build number to 780 would get displayed
		-- as "SP3a+", when it was actually SP3+). To avoid this, we will include an additional fake build
		-- number that combines the two.
		local SP_LOOKUP_TABLE_6_5 = { {201, "RTM"}, {213, "SP1"}, {240, "SP2"}, {258, "SP3"}, {281, "SP4"},
			{415, "SP5"}, {416, "SP5a"}, {417, "SP5/SP5a"}, }
		
		local SP_LOOKUP_TABLE_7 = { {623, "RTM"}, {699, "SP1"}, {842, "SP2"}, {961, "SP3"}, {1063, "SP4"}, }
		
		local SP_LOOKUP_TABLE_2000 = { {194, "RTM"}, {384, "SP1"}, {532, "SP2"}, {534, "SP2"}, {760, "SP3"},
			{766, "SP3a"}, {767, "SP3/SP3a"}, {2039, "SP4"}, }
		
		local SP_LOOKUP_TABLE_2005 = { {1399, "RTM"}, {2047, "SP1"}, {3042, "SP2"}, {4035, "SP3"}, }
		
		local SP_LOOKUP_TABLE_2008 = { {1600, "RTM"}, {2531, "SP1"}, {4000, "SP2"}, }
		
		local SP_LOOKUP_TABLE_2008R2 = { {1660, "RTM"}, }
	
	
		if ( not self.brandedVersion ) then
			self:__InferProductVersion()
		end
		
		local spLookupTable
		if self.brandedVersion == "6.5" then spLookupTable = SP_LOOKUP_TABLE_6_5
			elseif self.brandedVersion == "7.0" then spLookupTable = SP_LOOKUP_TABLE_7
			elseif self.brandedVersion == "2000" then spLookupTable = SP_LOOKUP_TABLE_2000
			elseif self.brandedVersion == "2005" then spLookupTable = SP_LOOKUP_TABLE_2005
			elseif self.brandedVersion == "2008" then spLookupTable = SP_LOOKUP_TABLE_2008
			elseif self.brandedVersion == "2008 R2" then spLookupTable = SP_LOOKUP_TABLE_2008R2
		end
		
		return spLookupTable
	
	end,
	
	
	--- Processes version data to determine (if possible) the product version,
	--  service pack level and patch status.
	__ParseVersionInfo = function(self)
	
		local spLookupTable = self:__GetSpLookupTable()
		
		if spLookupTable then
			
			local spLookupItr = 0
			-- Loop through the service pack levels until we find one whose revision
			-- number is the same as or lower than our revision number.
			while spLookupItr < #spLookupTable do
				spLookupItr = spLookupItr + 1
				
				if (spLookupTable[ spLookupItr ][1] == self.build ) then
					spLookupItr = spLookupItr
					break
				elseif (spLookupTable[ spLookupItr ][1] > self.build ) then
					-- The target revision number is lower than the first release
					if spLookupItr == 1 then
						self.servicePackLevel = "Pre-RTM"
					else
						-- we went too far - it's the previous SP, but with patches applied
						spLookupItr = spLookupItr - 1
					end
					break
				end
			end
			
			-- Now that we've identified the proper service pack level:
			if self.servicePackLevel ~= "Pre-RTM" then
				self.servicePackLevel = spLookupTable[ spLookupItr ][2]
				
				if ( spLookupTable[ spLookupItr ][1] == self.build ) then
					self.patched = false
				else
					self.patched = true
				end
			end
			
			-- Clean up some of our inferences. If the source of our revision number
			-- was the SSRP (SQL Server Browser) response, we need to recognize its
			-- limitations:
			--  * Versions of SQL Server prior to 2005 are reported with the RTM build
			--    number, regardless of the actual version (e.g. SQL Server 2000 is
			--    always 8.00.194).
			--  * Versions of SQL Server starting with 2005 (and going through at least
			--    2008) do better but are still only reported with the build number as
			--    of the last service pack (e.g. SQL Server 2005 SP3 with patches is
			--    still reported as 9.00.4035.00). 
			if ( self.source == "SSRP" ) then
				self.patched = nil
				
				if ( self.major <= 8 ) then
					self.servicePackLevel = nil
				end
			end
		end
		
		return true
	end,
	
	---
	ToString = function(self)
		local friendlyVersion = strbuf.new()
		if self.productName then
			friendlyVersion:concatbuf( self.productName )
			if self.servicePackLevel then
				friendlyVersion:concatbuf( " " )
				friendlyVersion:concatbuf( self.servicePackLevel )
			end
			if self.patched then
				friendlyVersion:concatbuf( "+" )
			end
		end
		
		return friendlyVersion:dump()
	end
}








--- Adds a label and value to an output table. If the value is a boolean, it is
--  converted to Yes/No; if the value is nil, nothing is added to the table. 
local function add_to_output_table( outputTable, outputLabel, outputData )

	if outputData ~= nil then
		if outputData == true then
			outputData = "Yes"
		elseif outputData == false then
			outputData = "No"
		end
		
		table.insert(outputTable, string.format( "%s: %s", outputLabel, outputData ) )
	end

end


--- Returns formatted output for the given version data
local function create_version_output_table( versionInfo )
	
	local versionResult = {}
	
	versionResult["name"] = "Version: " .. versionInfo:ToString()
	if ( versionInfo.source ~= "SSRP" ) then
		add_to_output_table( versionResult, "Version number", versionInfo.versionNumber )
	end
	add_to_output_table( versionResult, "Product", versionInfo.productName )
	add_to_output_table( versionResult, "Service pack level", versionInfo.servicePackLevel )
	add_to_output_table( versionResult, "Post-SP patches applied", versionInfo.patched )
	
	return versionResult

end


--- Returns formatted output for the given instance
local function create_instance_output_table( instance )

	-- if we didn't get anything useful (due to errors or the port not actually
	-- being SQL Server), don't report anything
	if not ( instance.name or instance.versionInfo ) then return nil end

	local instanceResult = {}

	if instance.name then
		instanceResult["name"] = string.format( "Instance name: %s", instance.name )
	end
	
	if instance.versionInfo then
		local versionResult = create_version_output_table( instance.versionInfo )
		table.insert( instanceResult, versionResult )
	end
	
	add_to_output_table( instanceResult, "TCP port", instance.port )
	add_to_output_table( instanceResult, "Named pipe", instance.pipe )
	add_to_output_table( instanceResult, "Clustered", instance.clustered )

	return instanceResult

end


--- Processes a single instance, attempting to determine its version, etc.
local function process_instance( instance )

	-- If this is set, we will rely only on the SSRP response from the SQL
	-- Server Monitor/Browser Service (UDP 1434, what mssql.Helper.Discover
	-- queried) and will not attempt to connect to each instance to get a
	-- more accurate version number.
	local ssrpOnly  = stdnse.get_script_args( "ms-sql-info.browseronly" )
	
	local ssnetlibStatus, version
	
	if ( instance.port and (not ssrpOnly) ) then
		ssnetlibStatus, version = Helper.GetInstanceVersion( instance.ip, instance.port )
		-- If the attempt to get the SSNetLib version fails, we'll use the
		-- (less-accurate) version from the SSRP response.
	end
	
	if ( ssrpOnly or (ssnetlibStatus == false) or (instance.port == nil) ) then
		if (ssnetlibStatus == false) then
			stdnse.print_debug( 1, "%s: Could not retrieve SSNetLib version for %s.", SCRIPT_NAME, instance.name or "instance" )
		elseif (instance.port == nil) then
			stdnse.print_debug( 1, "%s: No TCP port available to connect to for %s. ", SCRIPT_NAME, instance.name or "instance" )
		elseif ssrpOnly then
			stdnse.print_debug( 1, "%s: 'browseronly' argument set. Will not connect to instance TCP port.", SCRIPT_NAME )
		end
		
		if ( instance.version ) then
			stdnse.print_debug( 1, "%s: Using version number from SSRP response.", SCRIPT_NAME )
			version = SqlServerVersionInfo:new()
			version:SetVersionNumber( instance.version, "SSRP" )
		else
			version = nil
		end
	end
	
	if ( version ) then
		instance.versionInfo = version
	else
		stdnse.print_debug( 1, "%s: Version info could not be retrieved.", SCRIPT_NAME )
	end

end


--- Processes a set of instances, attempting to determine their versions, etc.
--  and then generating script output for them.
local function process_response( instanceList )

	if not instanceList then return end
	
	local scriptOutput, serverResult = {}, {}
	
	for _, instance in pairs( instanceList ) do
		if instance.servername then
			local serverName = instance.servername
			serverResult["name"] = string.format( "Server name: %s", serverName )
			break
		end
	end
	
	table.insert(scriptOutput, serverResult)
	
	for _, instance in pairs( instanceList ) do
		process_instance( instance )
		local instanceResult = create_instance_output_table( instance )
		table.insert(serverResult, instanceResult)
	end
	
	return scriptOutput
end


action = function( host, port )

	local status, serverList

	if ( (port.number == 1433 or port.service == "ms-sql-s") and port.protocol == "tcp" ) then
		if stdnse.get_script_args( "ms-sql-info.browseronly" ) then return end
		
		serverList = {}
		local instanceList, instance = {}, {}
		instance.ip = host.ip
		instance.port = port.number
		instanceList[ instance.port ] = instance
		serverList[ host.ip ] = instanceList
	elseif ( port.number == 1434 and port.protocol == "udp" ) then
		status, serverList = mssql.Helper.Discover( host, 1434 )
		if ( not(status) ) then return end
	end

	if ( not(serverList) ) then return end

	local result = process_response( serverList[host.ip] )
	if ( not(result) ) then return end

	nmap.set_port_state( host, port, "open")
	return stdnse.format_output( true, result )

end
